diff --git a/.configurations/configuration.dsc.yaml b/.configurations/configuration.dsc.yaml index 255da69a5bb7b..780b1dfa9596b 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 31d67db5fac9f..bc30d7dbe3b7b 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 0000000000000..7c6e13032ae8c --- /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 4d5b300eafbd5..c39a66311e4fc 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" @@ -645,7 +653,6 @@ "events", "fs", "fs/promises", - "graceful-fs", "http", "https", "minimist", @@ -667,6 +674,7 @@ "vscode-regexpp", "vscode-textmate", "worker_threads", + "@xterm/addon-clipboard", "@xterm/addon-image", "@xterm/addon-search", "@xterm/addon-serialize", @@ -1016,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": [] }, { @@ -1094,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 44514039e1e64..d0a2c7789977e 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 cfd9abc374a64..a61f9cfb13758 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 81fd351675132..7145de06db56c 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 e21061549d99e..e663372fad07d 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 d0e14e936c20f..52ac0d3ddcd9a 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/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml index f7392dc24a8c5..7db2cedf9df06 100644 --- a/.github/workflows/latest-release-monitor.yml +++ b/.github/workflows/latest-release-monitor.yml @@ -1,4 +1,9 @@ name: Latest Release Monitor + +permissions: + id-token: write + contents: read + on: schedule: - cron: 0/5 * * * * @@ -8,7 +13,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: @@ -22,6 +33,4 @@ jobs: - name: Run Latest Release Monitor uses: ./actions/latest-release-monitor with: - storageKey: ${{secrets.AZURE_BLOB_STORAGE_CONNECTION_STRING}} - appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 5860349a43758..ef775ce8fdfb7 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 361ac11b94667..2a26794c6b01b 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 0000000000000..d29de326c53a3 --- /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/.nvmrc b/.nvmrc index bc78e9f2695ea..48b14e6b2b56f 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.12.1 +20.14.0 diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts index 7280782c10a76..3fff7c5b63789 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,20 +84,11 @@ class ScriptCoverageTracker { } } - /** - * Generates the script's coverage for the test run. - * - * If a source location mapper is given, it assumes the `uri` is the mapped - * URI, and that any unmapped locations/outside the URI should be ignored. - */ - public report( + public *toDetails( uri: vscode.Uri, convert: OffsetToPosition, mapper: SourceLocationMapper | undefined, - item?: vscode.TestItem - ): V8CoverageFile { - const file = new V8CoverageFile(uri, item); - + ) { for (const range of this.coverage) { if (range.start === range.end) { continue; @@ -113,8 +100,8 @@ class ScriptCoverageTracker { 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); + 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; } @@ -123,28 +110,48 @@ class ScriptCoverageTracker { } 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) - ) + 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. + * + * If a source location mapper is given, it assumes the `uri` is the mapped + * URI, and that any unmapped locations/outside the URI should be ignored. + */ + public report( + uri: vscode.Uri, + convert: OffsetToPosition, + mapper: SourceLocationMapper | undefined, + items: Map, + ): V8CoverageFile { + 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 960dbcf634e3c..491f67ee30080 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/stackTraceParser.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.ts new file mode 100644 index 0000000000000..ca3236ce96a92 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/stackTraceParser.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. + *--------------------------------------------------------------------------------------------*/ + +// Copied from https://github.com/microsoft/vscode-js-debug/blob/1d104b5184736677ab5cc280c70bbd227403850c/src/common/stackTraceParser.ts#L18 + +// Either match lines like +// " at fulfilled (/Users/roblou/code/testapp-node2/out/app.js:5:58)" +// or +// " at /Users/roblou/code/testapp-node2/out/app.js:60:23" +// and replace the path in them +const re1 = /^(\W*at .*\()(.*):(\d+):(\d+)(\))$/; +const re2 = /^(\W*at )(.*):(\d+):(\d+)$/; + +const getLabelRe = /^\W*at (.*) \($/; + +/** + * Parses a textual stack trace. + */ +export class StackTraceParser { + /** Gets whether the stacktrace has any locations in it. */ + public static isStackLike(str: string) { + return re1.test(str) || re2.test(str); + } + constructor(private readonly stack: string) { } + + /** Iterates over segments of text and locations in the stack. */ + *[Symbol.iterator]() { + for (const line of this.stack.split('\n')) { + const match = re1.exec(line) || re2.exec(line); + if (!match) { + yield line + '\n'; + continue; + } + + const [, prefix, url, lineNo, columnNo, suffix] = match; + if (prefix) { + yield prefix; + } + + yield new StackTraceLocation(getLabelRe.exec(prefix)?.[1], url, Number(lineNo), Number(columnNo)); + + if (suffix) { + yield suffix; + } + + yield '\n'; + } + } +} + +export class StackTraceLocation { + constructor( + public readonly label: string | undefined, + public readonly path: string, + public readonly lineBase1: number, + public readonly columnBase1: number, + ) { } +} \ No newline at end of file diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index 74013dcd56105..f8e70b4d9cdd5 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -16,6 +16,7 @@ import * as vscode from 'vscode'; import { istanbulCoverageContext, PerTestCoverageTracker } from './coverageProvider'; import { attachTestMessageMetadata } from './metadata'; import { snapshotComment } from './snapshot'; +import { StackTraceLocation, StackTraceParser } from './stackTraceParser'; import { StreamSplitter } from './streamSplitter'; import { getContentFromFilesystem } from './testTree'; import { IScriptCoverage } from './v8CoverageWrangling'; @@ -288,8 +289,8 @@ export async function scanTestOutput( enqueueExitBlocker( (async () => { - const location = await tryDeriveStackLocation(store, rawErr, tcase!); - let message: vscode.TestMessage; + const stackInfo = await deriveStackLocations(store, rawErr, tcase!); + let message: vscode.TestMessage2; if (hasDiff) { message = new vscode.TestMessage(tryMakeMarkdown(err)); @@ -310,7 +311,8 @@ export async function scanTestOutput( ); } - message.location = location ?? testFirstLine; + message.location = stackInfo.primary ?? testFirstLine; + message.stackTrace = stackInfo.stack; task.failed(tcase!, message, duration); })() ); @@ -424,36 +426,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 +489,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) { @@ -558,47 +610,51 @@ async function replaceAllLocations(store: SourceMapStore, str: string) { return values.join(''); } -async function tryDeriveStackLocation( +async function deriveStackLocations( store: SourceMapStore, stack: string, tcase: vscode.TestItem ) { locationRe.lastIndex = 0; - return new Promise(resolve => { - const matches = [...stack.matchAll(locationRe)]; - let todo = matches.length; - if (todo === 0) { - return resolve(undefined); + const locationsRaw = [...new StackTraceParser(stack)].filter(t => t instanceof StackTraceLocation); + const locationsMapped = await Promise.all(locationsRaw.map(async location => { + const mapped = location.path.startsWith('file:') ? await store.getSourceLocation(location.path, location.lineBase1 - 1, location.columnBase1 - 1) : undefined; + const stack = new vscode.TestMessageStackFrame(location.label || '', mapped?.uri, mapped?.range.start || new vscode.Position(location.lineBase1 - 1, location.columnBase1 - 1)); + return { location: mapped, stack }; + })); + + let best: undefined | { location: vscode.Location; score: number }; + for (const { location } of locationsMapped) { + if (!location) { + continue; } - - let best: undefined | { location: vscode.Location; i: number; score: number }; - for (const [i, match] of matches.entries()) { - deriveSourceLocation(store, match) - .catch(() => undefined) - .then(location => { - if (location) { - let score = 0; - if (tcase.uri && tcase.uri.toString() === location.uri.toString()) { - score = 1; - if (tcase.range && tcase.range.contains(location?.range)) { - score = 2; - } - } - if (!best || score > best.score || (score === best.score && i < best.i)) { - best = { location, i, score }; - } - } - - if (!--todo) { - resolve(best?.location); - } - }); + let score = 0; + if (tcase.uri && tcase.uri.toString() === location.uri.toString()) { + score = 1; + if (tcase.range && tcase.range.contains(location?.range)) { + score = 2; + } + } + if (!best || score > best.score) { + best = { location, score }; } - }); + } + + return { stack: locationsMapped.map(s => s.stack), primary: best?.location }; } async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArray) { 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; +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts index 8a76cefe36a79..954b847f4a8b7 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts @@ -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; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json index 0183a2ff57e20..1f087a4161541 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -11,6 +11,7 @@ "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.testMessageStackTrace.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 2ac7d69958242..c5dda6b26af1b 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:\"July 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index d7836922badbf..e1c0a9fce02b8 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 3bf56fffce483..0b260270ed7b0 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 091c6e8a88635..1e539f4c3c1d9 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:\"July 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 5d5d1f27589fc..9255a781bd487 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,13 +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", + "inlineChat.experimental.textButtons": true, "typescript.enablePromptUseWorkspaceTsdk": true } diff --git a/.yarnrc b/.yarnrc index b40fb7e7f58ab..a94956590fa08 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "29.4.0" -ms_build_id "9593362" +target "30.1.2" +ms_build_id "9759760" runtime "electron" build_from_source "true" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 2e3f2610f46d2..fe3cf5c4a9b0d 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.6 - MIT +go-syntax 0.6.8 - MIT https://github.com/worlpaker/go-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 4a19f329eb108..d7d415d321357 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2024-05-25T03:29:59.419Z +2024-07-04T16:31:11.121Z diff --git a/build/.webignore b/build/.webignore index 88fe96f5cc16d..15935edce8a61 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/alpine/cli-build-alpine.yml b/build/azure-pipelines/alpine/cli-build-alpine.yml index a6442dfe29032..2c3b653ce7eb9 100644 --- a/build/azure-pipelines/alpine/cli-build-alpine.yml +++ b/build/azure-pipelines/alpine/cli-build-alpine.yml @@ -16,7 +16,7 @@ steps: inputs: versionSource: fromFile versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + #nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: # Install yarn as the ARM64 build agent is using vanilla Ubuntu diff --git a/build/azure-pipelines/cli/cli-compile.yml b/build/azure-pipelines/cli/cli-compile.yml index 267682f7f6d07..e77ba78a999fd 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/cli-build-darwin.yml b/build/azure-pipelines/darwin/cli-build-darwin.yml index 1d8dffc464d38..ad901c8de1d0d 100644 --- a/build/azure-pipelines/darwin/cli-build-darwin.yml +++ b/build/azure-pipelines/darwin/cli-build-darwin.yml @@ -16,7 +16,7 @@ steps: inputs: versionSource: fromFile versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + #nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - template: ../cli/cli-apply-patches.yml@self diff --git a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist index 1cc1a152c74ab..48f7bf5cece0e 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-cli-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml index 80e90a52bac8f..e6cff11dc7d35 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml @@ -9,7 +9,7 @@ steps: inputs: versionSource: fromFile versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + #nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - script: node build/setup-npm-registry.js $NPM_REGISTRY build condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) diff --git a/build/azure-pipelines/darwin/product-build-darwin-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-sign.yml index fb8cf8341c360..e8f1bdf511ce2 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-sign.yml @@ -3,7 +3,7 @@ steps: inputs: versionSource: fromFile versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + #nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - task: UseDotNet@2 inputs: diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index f8b201f40d46f..45367dde187f0 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -3,7 +3,7 @@ steps: inputs: versionSource: fromFile versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + #nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - template: ../distro/download-distro.yml@self diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 8b4bda1c6a2f2..a49d5e4abd65d 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -20,7 +20,7 @@ steps: inputs: versionSource: fromFile versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + #nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - template: ../distro/download-distro.yml@self @@ -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/distro-build.yml b/build/azure-pipelines/distro-build.yml index ee5dd5d99197f..2e35404ac9b0e 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -11,5 +11,5 @@ steps: inputs: versionSource: fromFile versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download + #nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - template: ./distro/download-distro.yml@self 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 dc8424f26eea4..26f02657e10af 100644 --- a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml +++ b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml @@ -84,16 +84,6 @@ steps: imageName: vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) containerCommand: uname - - ${{ if eq(parameters.VSCODE_ARCH, 'armhf') }}: - - task: Docker@1 - displayName: "Pull Docker image" - inputs: - azureSubscriptionEndpoint: "vscode-builds-subscription" - azureContainerRegistry: vscodehub.azurecr.io - command: "Run an image" - imageName: vscode-linux-build-agent:bionic-arm32v7 - containerCommand: uname - - script: | set -e # To workaround the issue of yarn not respecting the registry value from .npmrc @@ -129,18 +119,8 @@ steps: VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) - ${{ if eq(parameters.VSCODE_ARCH, 'armhf') }}: - 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 +152,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 +174,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.17" \ + 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.yml b/build/azure-pipelines/linux/product-build-linux.yml index 352b31360f839..d1d6bdb9191b9 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/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index 9bfbf9ab41a48..949b5f371ba9f 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -13,7 +13,7 @@ SYSROOT_ARCH="$SYSROOT_ARCH" node -e '(async () => { const { getVSCodeSysroot } if [ "$npm_config_arch" == "x64" ]; then if [ "$(echo "$@" | grep -c -- "--only-remote")" -eq 0 ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/122.0.6261.156/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/124.0.6367.243/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 \ @@ -25,9 +25,9 @@ if [ "$npm_config_arch" == "x64" ]; then # Set compiler toolchain # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:build/config/c++/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:build/config/c++/BUILD.gn export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" export CXXFLAGS="-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 -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" @@ -54,17 +54,15 @@ elif [ "$npm_config_arch" == "arm64" ]; then export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" fi elif [ "$npm_config_arch" == "arm" ]; then - if [ "$(echo "$@" | grep -c -- "--only-remote")" -eq 0 ]; then - # Set compiler toolchain for client native modules - export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc - export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" + # Set compiler toolchain for client native modules + export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" - # Set compiler toolchain for remote server - export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" - fi + # Set compiler toolchain for remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" fi diff --git a/build/azure-pipelines/linux/verify-glibc-requirements.sh b/build/azure-pipelines/linux/verify-glibc-requirements.sh index f07c0ba71b0e4..19482c242ea14 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 0508059fcd7b1..0c4f98aa5116c 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -167,7 +167,7 @@ resources: 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 41c33f3f26512..9a3748ed6fc19 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 927cd5e04ae68..af20a305d9cdc 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' - -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" - - - 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" - - - 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(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication - - - 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 - - - task: CodeQL3000Init@0 - displayName: CodeQL Initialize - condition: eq(variables['Codeql.enabled'], 'True') - - - 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 - - - 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 - - - powershell: yarn compile - displayName: Compile - - - task: CodeQL3000Finalize@0 - displayName: CodeQL Finalize - condition: eq(variables['Codeql.enabled'], 'True') - - - powershell: yarn gulp "vscode-symbols-win32-$(VSCODE_ARCH)" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Download Symbols - - - task: PSScriptAnalyzer@1 - inputs: - Path: '$(Build.SourcesDirectory)' - Settings: required - Recurse: true - - - 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: 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' + type: string + default: x64 + +steps: + - 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" + + - 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 + + - task: npmAuthenticate@0 + inputs: + workingFile: .npmrc + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne('${{ parameters.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 + + - pwsh: | + $includes = @' + { + 'target_defaults': { + 'conditions': [ + ['OS=="win"', { + 'msvs_configuration_attributes': { + 'SpectreMitigation': 'Spectre' + }, + 'msvs_settings': { + 'VCCLCompilerTool': { + 'AdditionalOptions': [ + '/Zi', + '/FS' + ], + }, + 'VCLinkerTool': { + 'AdditionalOptions': [ + '/profile' + ] + } + } + }] + ] + } + } + '@ + + 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" + 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-quality + displayName: Mixin distro quality + env: + VSCODE_QUALITY: stable + + - powershell: yarn compile + displayName: Compile + + - powershell: yarn gulp "vscode-symbols-win32-${{ parameters.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: CopyFiles@2 + displayName: 'Collect Symbols for API Scan' + inputs: + SourceFolder: $(Agent.BuildDirectory) + Contents: 'scanbin\**\*.pdb' + TargetFolder: '$(agent.builddirectory)\symbols' + flattenFolders: true + condition: succeeded() + + # - 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: 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 34c2005a30fde..5b6cd3ed1fd55 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 416d0eec408f7..030cc8f0e5a6c 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) => { + 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 }; + } + } + } + })), - es.merge( - vfs.src('out-vscode-web-min/nls.metadata.json', { base: 'out-vscode-web-min' }), + // 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') { - return { vscode: parsedJson }; - } + 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; + // 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.header.json': + parsedJson = { header: parsedJson }; + break; - case 'nls.metadata.json': { - // put nls.metadata.json content in Core NlsMetadata format - const modules = Object.keys(parsedJson); + 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 json: NlsMetadata = { + keys: {}, + messages: {}, + bundles: { + main: [] } - parsedJson = json; - break; + }; + 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 }; - }, - })) + // 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 bf43d9212cf3d..a522e845f3bee 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 a3b251b71ac17..ce791c094e6b8 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/electron.txt b/build/checksums/electron.txt index a80aa1531f168..52950abd800f5 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -3d3d8bb185d7b63b0db910661fdd69d6381afb8c97742bbd2526a9c932e1f8ca *chromedriver-v29.4.0-darwin-arm64.zip -c3d075943d87604ffa50382cc8d5798485349544ca391cab88c892f889d3b14c *chromedriver-v29.4.0-darwin-x64.zip -6d62d2dba55e4419fa003d45f93dad1324ec29a4d3eb84fd9fd5fd7a64339389 *chromedriver-v29.4.0-linux-arm64.zip -81bb3d362331c7296f700b1b0e8f07c4c7739b1151f698cd56af927bedda59e7 *chromedriver-v29.4.0-linux-armv7l.zip -ab593cc39aefac8c5abd259e31f6add4b2b70c52231724a6c08ac1872b4a0edf *chromedriver-v29.4.0-linux-x64.zip -705d42ccc05b2c48b0673b9dcf63eb78772bb79dba078a523d384ed2481bc9c0 *chromedriver-v29.4.0-mas-arm64.zip -956a7caa28eeeb0c02eb7638a53215ffd89b4f12880f0893ff10f497ca1a8117 *chromedriver-v29.4.0-mas-x64.zip -1f070176aa33e0139d61a3d758fd2f015f09bb275577293fe93564749b6310ba *chromedriver-v29.4.0-win32-arm64.zip -38a71526d243bcb73c28cb648bd4816d70b5e643df52f9f86a83416014589744 *chromedriver-v29.4.0-win32-ia32.zip -f90750d3589cb3c9f6f0ebc70d5e025cf81c382e8c23fa47a54570696a478ef0 *chromedriver-v29.4.0-win32-x64.zip -05dffc90dd1341cc7a6b50127985e4e217fef7f50a173c7d0ff34039dd2d81b6 *electron-api.json -7f63f7cf675ba6dec3a5e4173d729bd53c75f81e612f809641d9d0c4d9791649 *electron-v29.4.0-darwin-arm64-dsym-snapshot.zip -aa29530fcafa4db364978d4f414a6ec2005ea695f7fee70ffbe5e114e9e453f0 *electron-v29.4.0-darwin-arm64-dsym.zip -8d12fb6d9bcdf5bbfc93dbcd1cac348735dc6f98aa450ee03ec7837a01a8a938 *electron-v29.4.0-darwin-arm64-symbols.zip -c16d05f1231bb3c77da05ab236b454b3a2b6a642403be51e7c9b16cd2c421a19 *electron-v29.4.0-darwin-arm64.zip -2dfc1017831ab2f6e9ddb575d3b9cff5a0d56f16a335a3c0df508e964e2db963 *electron-v29.4.0-darwin-x64-dsym-snapshot.zip -025de6aa39d98762928e1b700f46177e74be20101b27457659b938e2c69db326 *electron-v29.4.0-darwin-x64-dsym.zip -ec4eb0a618207233985ceaab297be34b3d4f0813d88801d5637295b238dd661a *electron-v29.4.0-darwin-x64-symbols.zip -8ed7924f77a5c43c137a57097c5c47c2e8e9a78197e18af11a767c98035c123e *electron-v29.4.0-darwin-x64.zip -bde1772fa8ac4850e108012a9edd3bd93472bad8f68ddd55fca355dad81dde4f *electron-v29.4.0-linux-arm64-debug.zip -dfe7852a7423196efb2205c788d942db3ffc9de6ce52577e173bcf7ca6973d48 *electron-v29.4.0-linux-arm64-symbols.zip -c3764d6c3799950e3418e8e5a5a5b2c41abe421dd8bcdebf054c7c85798d9860 *electron-v29.4.0-linux-arm64.zip -bde1772fa8ac4850e108012a9edd3bd93472bad8f68ddd55fca355dad81dde4f *electron-v29.4.0-linux-armv7l-debug.zip -360668ba669cb2c01c2f960cdee76c29670e6ce907ccc0718e971a04af594ce9 *electron-v29.4.0-linux-armv7l-symbols.zip -c5e92943ad78b4e41a32ae53c679e148ea2ae09f95f914b1834dbdbae578ba91 *electron-v29.4.0-linux-armv7l.zip -375be885426bcbd272bd068bfcef41a83296c2f8e61e633233d2a9e9a69242fc *electron-v29.4.0-linux-x64-debug.zip -847e0f75624616c2918b33de2eefeec63419bd250685610d3f52fa115527d2b9 *electron-v29.4.0-linux-x64-symbols.zip -91e5eb374c2c85a07c2d4e99a89eb18515ff0169a49c3fa75289800e1225729e *electron-v29.4.0-linux-x64.zip -098f973537c3d9679a69409d0b84bcc1a6113bb2002ee60068e2c22f335a3855 *electron-v29.4.0-mas-arm64-dsym-snapshot.zip -2724aa32eb441eea21680d95fc1efdd75ac473fa19623c7acf3d546419e96154 *electron-v29.4.0-mas-arm64-dsym.zip -98dd81914752a57da4cbaad1f0aa94b16335f9b8f997be9aa049be90b96b2886 *electron-v29.4.0-mas-arm64-symbols.zip -fd2663f65c1f995304e3eb65870b7146adfefef07cf82bf44de75855fd4f36e8 *electron-v29.4.0-mas-arm64.zip -237983b2169e69bb73aa0987e871e3e486755904b71ebe36c3e902377f92754a *electron-v29.4.0-mas-x64-dsym-snapshot.zip -a5d59599827d32ef322b99eee8416e39235f4c7a0ada78342a885665e0b732dd *electron-v29.4.0-mas-x64-dsym.zip -5182e7697ac0591e0b95c33f70316af24093c9100f442be2cee0039660e959ac *electron-v29.4.0-mas-x64-symbols.zip -e0ee7057aff0240a70b9ed75ff44d55aeae9af67fbc8915f741711a8bb6fe744 *electron-v29.4.0-mas-x64.zip -2802872dfc6de0f0e2e8cef9d2f4f384e3d82b20ad36fc981c4e725dd2f2abcd *electron-v29.4.0-win32-arm64-pdb.zip -d49c954dc25ae9e4c75e61af80b9718014c52f016f43a29071913f0e7100c7bd *electron-v29.4.0-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.4.0-win32-arm64-toolchain-profile.zip -483d692efbe4fb1231ff63afb8a236b2e22b486fbe5ac6abbc8b208abf94a4d3 *electron-v29.4.0-win32-arm64.zip -98458f49ba67a08e473d475a68a2818d9df076a5246fbc9b45403e8796f9d35b *electron-v29.4.0-win32-ia32-pdb.zip -69d505d4ae59d9dddf83c4e530e45dd7c5bc64d6da90cf4f851e523be9e51014 *electron-v29.4.0-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.4.0-win32-ia32-toolchain-profile.zip -d5a21a17a64e9638f49f057356af23b51f56bd6a7fea3c2e0a28ff3186a7bc41 *electron-v29.4.0-win32-ia32.zip -521ee7b3398c4dc395b43dac86cd099e86a6123de2b43636ee805b7da014ed3f *electron-v29.4.0-win32-x64-pdb.zip -e33848ebd6c6e4ce431aa367bef887050947a136e883677cfc524ca5cabc1e98 *electron-v29.4.0-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v29.4.0-win32-x64-toolchain-profile.zip -e4ef85aa3608221f8a3e011c1b1c2d2d36093ad19bda12d16b3816929fb6c99b *electron-v29.4.0-win32-x64.zip -707ee08593289ee83514b4fc55123611309f995788f38a5ec03e285741aac1c8 *electron.d.ts -281b5f4a49de55fdb86b1662530f07f2ced1252c878eb7a941c88ede545339e0 *ffmpeg-v29.4.0-darwin-arm64.zip -0b735912df9b2ff3d03eb23942e03bc0116d82f1291d0a45cbde14177c2f3066 *ffmpeg-v29.4.0-darwin-x64.zip -4e2ba537d7c131abbd34168bce2c28cc9ef6262b217d5f4085afccfdf9635da6 *ffmpeg-v29.4.0-linux-arm64.zip -4aa56ad5d849f4e61af22678a179346b68aec9100282e1b8a43df25d95721677 *ffmpeg-v29.4.0-linux-armv7l.zip -0558e6e1f78229d303e16d4d8c290794baa9adc619fdd2ddccadb3ea241a1df4 *ffmpeg-v29.4.0-linux-x64.zip -224f15d8f96c75348cd7f1b85c4eab63468fae1e50ff4b1381e08011cf76e4f7 *ffmpeg-v29.4.0-mas-arm64.zip -175ec79f0dc4c5966d9a0ca6ec1674106340ecc64503585c12c2f854249af06f *ffmpeg-v29.4.0-mas-x64.zip -5fa13744b87fef1bfd24a37513677f446143e085504541f8ce97466803bd1893 *ffmpeg-v29.4.0-win32-arm64.zip -d7ba316bb7e13025c9db29e0acafebb540b7716c9f111e469733615d8521186a *ffmpeg-v29.4.0-win32-ia32.zip -35c70a28bcfd4f0b1f8c985d3d1348936bd60767d231ce28ba38f3daeeef64bb *ffmpeg-v29.4.0-win32-x64.zip -8c7228ea0ecab25a1f7fcd1ba9680684d19f9671a497113d71a851a53867b048 *hunspell_dictionaries.zip -7552547c8d585b9bc43518d239d7ce3ad7c5cad0346b07cdcfc1eab638b2b794 *libcxx-objects-v29.4.0-linux-arm64.zip -76054a779d4845ad752b625213ce8990f08dcc5b89aa20660dd4f2e817ba30a8 *libcxx-objects-v29.4.0-linux-armv7l.zip -761c317a9c874bd3d1118d0ecad33c4be23727f538cfbb42a08dd87c68da6039 *libcxx-objects-v29.4.0-linux-x64.zip -f98f9972cc30200b8e05815f5a9cd5cec04bdeee0e48ae2143cdaeff5db9d71d *libcxx_headers.zip -f0b0dd2be579baaf97901322ef489d03fae69a0b8524ea77b24fb3c896f73dd9 *libcxxabi_headers.zip -5da864ea23d70538298a40e0d037a5a461a6b74984e72fd4f0cd20904bccaed1 *mksnapshot-v29.4.0-darwin-arm64.zip -bde97bd7c69209ed6bf4cf1cdf7de622e3a9f50fe6b4dc4b5618eee868f47c62 *mksnapshot-v29.4.0-darwin-x64.zip -a3df9b9e6ef14efe5827d0256d8ecaebe6d8be130cfc3faac0dea76eb53b9b11 *mksnapshot-v29.4.0-linux-arm64-x64.zip -648b9dbca21194d663ddb706e6086a166e691263c764c80f836ae02c27e3657a *mksnapshot-v29.4.0-linux-armv7l-x64.zip -e7a4201cda3956380facc2b5b9d0b1020cc5e654fba44129fc7429a982411cc1 *mksnapshot-v29.4.0-linux-x64.zip -ffb44c45733675e0378f45fce25dafa95697d0c86179f8e46742ada16bc11aa1 *mksnapshot-v29.4.0-mas-arm64.zip -0242da3ca193206e56b88eb108502244bae35dcc587210bd0a32d9fa4cb71041 *mksnapshot-v29.4.0-mas-x64.zip -1445806dca6effbc60072bbde7997cefb62bdb7a9e295a090d26f27c3882685f *mksnapshot-v29.4.0-win32-arm64-x64.zip -09599adc3afb0a13ae87fc4b8ab97c729fe3689faa6a4f5f7a4a3cf0d9cc49d3 *mksnapshot-v29.4.0-win32-ia32.zip -84f80683d95665d29284386509bb104e840ff0b797bfbbd19da86b84d370aa49 *mksnapshot-v29.4.0-win32-x64.zip +cdf0522dacc5fdf75a9a4ca9a20f049793ef8bae2b04e37f02e8923fcecd4c76 *chromedriver-v30.1.2-darwin-arm64.zip +f739afd34c48aae18da1d9dffb2332f0c2b2e27ff2056ac619e0a8c9414618f0 *chromedriver-v30.1.2-darwin-x64.zip +c2a220a316c268984bb8135975f28adecc392cf5cd2244af8cc21d60018a6a10 *chromedriver-v30.1.2-linux-arm64.zip +fad358f076caff4eecc3d8b63cea2e109cc0ff8b4632bb4edd21a7c7721bc428 *chromedriver-v30.1.2-linux-armv7l.zip +addc230541e9ee44b311fba9e900c5cb7d8b4d64b79a6d5dae68a71ced1c4611 *chromedriver-v30.1.2-linux-x64.zip +e2d0876fac8af41e0dd9c1b14c5315b426c55217e54e2b4ca5e28faae1b19557 *chromedriver-v30.1.2-mas-arm64.zip +c705d0b74a4658c197a87ed1e9e2509e55186769376b40493ec68b7cbb36c312 *chromedriver-v30.1.2-mas-x64.zip +8bbf9ccb789b236dec3e871d2499c14926a4c2dd3c865f8c7316ba3aa5b3f58f *chromedriver-v30.1.2-win32-arm64.zip +75af4b07bbd3a8fd7e18d63eb936e11054d01b52d438e4f7b7c5e6b82a41dec3 *chromedriver-v30.1.2-win32-ia32.zip +2d306df2c66314be12df78b6139ebf2d616463efdf4017473330d87a61954c3a *chromedriver-v30.1.2-win32-x64.zip +1990fe83ca25b990773ce099b90fb804fc026c1398279731229ba37d02c23000 *electron-api.json +64360b0db764c1bf16a8ab810a25c03f68873691d2897b360281bc50a645b6bb *electron-v30.1.2-darwin-arm64-dsym-snapshot.zip +3192307419ee2bafc3c99c62d79dbd2e6cba5815ae245be994f0b1e1d7fedb46 *electron-v30.1.2-darwin-arm64-dsym.zip +823eadc46a498af7433dae2ace1c8f0b2b0c8cbf98204fed0fdce8110ddafbc1 *electron-v30.1.2-darwin-arm64-symbols.zip +3c651624b1605411e595a6e9f6e874effb947c80eda4b8d0bb7d2972ff6ff242 *electron-v30.1.2-darwin-arm64.zip +fa5cbbc3e7760907229fa0753c3faa2b43e09bcb987b6c3f693e0030aa65e62b *electron-v30.1.2-darwin-x64-dsym-snapshot.zip +d97087f4c7e41fd67b2f0f7aa623ccf1effdbb94133b883dbae1e4c42a576b03 *electron-v30.1.2-darwin-x64-dsym.zip +a129109ed6ca23f66d625ebff9e57be117bc0a32c4f4348e78c1ad7dd41c4189 *electron-v30.1.2-darwin-x64-symbols.zip +8645e10af9b047c765a6cc880f9fa53f266e618569eaf65c0ea9fa1058be20f7 *electron-v30.1.2-darwin-x64.zip +afa016399f57bbbb658238dd715ef2a66790602ff46514e9cd99f2e078789c7d *electron-v30.1.2-linux-arm64-debug.zip +aec05a3e46a83d7c3e502b04245d3d6d2db8d5789a4dcc991f7cf6e7e3cd7036 *electron-v30.1.2-linux-arm64-symbols.zip +953c51413abfd62efdba070f99c961202ce5ef5e77c5cee5eaaf097ac2f5bc9b *electron-v30.1.2-linux-arm64.zip +afa016399f57bbbb658238dd715ef2a66790602ff46514e9cd99f2e078789c7d *electron-v30.1.2-linux-armv7l-debug.zip +04a6851e218c9a6f70870a341beea1b194a94d2d78f3a283065a34654cce7e30 *electron-v30.1.2-linux-armv7l-symbols.zip +677d6b4e6721ffb27b0037628c235cd0a9f10104beabb2b6c67d4b0328a9d001 *electron-v30.1.2-linux-armv7l.zip +1b6d8926e2c7cacbb33e56259ebe908c52e2a6dbbe37f0def043121f228a3a37 *electron-v30.1.2-linux-x64-debug.zip +dc4b526b02ec028d20836ee4617335d0569170846959761c3e8dc615d668f596 *electron-v30.1.2-linux-x64-symbols.zip +724aeca4b2f428f544e9b7e5e52e2074458c2e198f588530819cd0318af8599d *electron-v30.1.2-linux-x64.zip +15b10a6cf6a1ea029792282088647abbf58b415fdb4dd004c2c67c4a8f216ef3 *electron-v30.1.2-mas-arm64-dsym-snapshot.zip +00dbfc36c8ff6ecfbb8b01b51fee41f876bf3456017a8cc694a0c56608061f5e *electron-v30.1.2-mas-arm64-dsym.zip +e402b68cad20ee60cce376dacf0e2e72f1fcd0b6359dda7789626dca4b101e8b *electron-v30.1.2-mas-arm64-symbols.zip +8e59fc7c6df96a029310d6f7769e0c76592dc746d31764b35460de129231d12b *electron-v30.1.2-mas-arm64.zip +e8bfe6b58d2767dd52a7668df380be9c786abed0b25d642bee70c278758d2e77 *electron-v30.1.2-mas-x64-dsym-snapshot.zip +418f32558f9a107ebc942666a6ca680874db8ed438ae3f0064255abe0f9ce77e *electron-v30.1.2-mas-x64-dsym.zip +6d05da37cb39c664a764c879216e762efdb66f97734e42bbaa8f115b11fd3c87 *electron-v30.1.2-mas-x64-symbols.zip +af7b85a28593227add7e595e01d570e19512b40a29599ec007ef7cd4b5a11435 *electron-v30.1.2-mas-x64.zip +580c9b9fee6bacfdc4d3a1118953ba0096bcc19d28b1d804d72d40c5caac8d81 *electron-v30.1.2-win32-arm64-pdb.zip +dbb0ce79933c3688b7fa2bf04ce083d6e8da0a8c07b5104f53805e2e92679cd4 *electron-v30.1.2-win32-arm64-symbols.zip +7351fa5cb892853a7d4b67d8858d0f9cc6506c554a2e42c9ad7e8d5e29ae2743 *electron-v30.1.2-win32-arm64-toolchain-profile.zip +1b9035b541999e0cedcfe3893bb3c1497435494279d599c4e9300fe204f4d560 *electron-v30.1.2-win32-arm64.zip +3cb6869a69d118488dc48ed573b3fc9445bea33cf0a04338ca1b8000b4eaf516 *electron-v30.1.2-win32-ia32-pdb.zip +3f353849ef21506d5ca7a2c26df84d7c744ef1795acd33307f501764dfbe9bc1 *electron-v30.1.2-win32-ia32-symbols.zip +7351fa5cb892853a7d4b67d8858d0f9cc6506c554a2e42c9ad7e8d5e29ae2743 *electron-v30.1.2-win32-ia32-toolchain-profile.zip +431aff46eca9ddb726af3c8d6f4bbb72158e5c872c1b8bee221b3df0a8e94947 *electron-v30.1.2-win32-ia32.zip +85428715d302d4c97e47ca0b6409497191846baccbc36debf895cde724f9445f *electron-v30.1.2-win32-x64-pdb.zip +a5156829bc0caab5c70e6cd352941b6fb7b1e396d7869298c1ceaa69d742e3dc *electron-v30.1.2-win32-x64-symbols.zip +7351fa5cb892853a7d4b67d8858d0f9cc6506c554a2e42c9ad7e8d5e29ae2743 *electron-v30.1.2-win32-x64-toolchain-profile.zip +d400ed1aca2b7b1093aee8b5a8544b112e9f81a570cf77f6bdfe019ceb003f7f *electron-v30.1.2-win32-x64.zip +3187cdd642b968e17a768a3fdaff44bedb69954be3d88e7ad55aac9787b70485 *electron.d.ts +4c8bd4215102f563cf0464ae4edd4022921c53e0ebd4bb6b6f7f7434d50081aa *ffmpeg-v30.1.2-darwin-arm64.zip +2b8c57755ccb64a64540433d3cfbfecd29d07e28bf23c5f08a2edd0e2333a645 *ffmpeg-v30.1.2-darwin-x64.zip +c6f69859dc2ca04daa7c218936e5bc044fc19582423db954c5e8664ce7f331df *ffmpeg-v30.1.2-linux-arm64.zip +12f760ce312e4ee98ddd9496600ca9a1468e28f0ed6d41c6b9284dec841f550f *ffmpeg-v30.1.2-linux-armv7l.zip +c7207cc057849033d550d6897a53f7abee455f31b8f571c3c57a01f2871ff5af *ffmpeg-v30.1.2-linux-x64.zip +447b19465677636261ce47cc256f221c660e66cd136b33c3c742b8d23481b924 *ffmpeg-v30.1.2-mas-arm64.zip +e44fcdb5d0e62d328c0bb5ebeed54f388b09591a33e4dc1698c421e4c9d881dc *ffmpeg-v30.1.2-mas-x64.zip +b470cc217c4f06b3fe4edf6695b4762e7c926d07c1f41c3cab0ddb1d71ce8ce4 *ffmpeg-v30.1.2-win32-arm64.zip +92f1743c16210a77d07e9553cf96ccfb4e6985cc50ee831b5b075589aa0ebb05 *ffmpeg-v30.1.2-win32-ia32.zip +b2fa79b739023651f0551ca06ac5da7b47acaaf8b09ccdedf7081fc3ea824a80 *ffmpeg-v30.1.2-win32-x64.zip +b2b562a45ae4a2d40bf039ed1c707a7875b9e893fc8c6a0044d536b0f9968629 *hunspell_dictionaries.zip +08da936356d1321eec550c30a9208750773d86e3be0b7fe4baf36c72a8609c20 *libcxx-objects-v30.1.2-linux-arm64.zip +8aa302ac17f6ac44f756cb9219f18d01d267d43f9af2dfd8d4626e9d01e584fa *libcxx-objects-v30.1.2-linux-armv7l.zip +1ab04d6cec407930a2051761e6114cb2cd6418e2103d32e835246d06c696b427 *libcxx-objects-v30.1.2-linux-x64.zip +4db17e017bdb818cca5d8e08a78fe54fb907c3cf05defd1b8f086b413075357a *libcxx_headers.zip +209f20bd3ee59fa7c85b0789d8e45168583407c9fb5bd2eba446c1e0796c4a7b *libcxxabi_headers.zip +4dcc59b7c66b9ccc881dbdfeba9de707f693fd839a8e764d34bb66dec7e3e63c *mksnapshot-v30.1.2-darwin-arm64.zip +582886552cbc227eb71f5047d00ac62baeb1912a52a8f5132953b94f54d41dd4 *mksnapshot-v30.1.2-darwin-x64.zip +639fde4957353d48be54b9075c0894b7529210f0bb3a2f9cad81ba2deb415080 *mksnapshot-v30.1.2-linux-arm64-x64.zip +1fec2ae49a9f03117a5e5b637866e5aa270da2fa3a007d0314c8a39ed392230f *mksnapshot-v30.1.2-linux-armv7l-x64.zip +494f86a178a2b14101c6deccf7c2ae88c690c7ce8445b63854a2e0525d69aaa0 *mksnapshot-v30.1.2-linux-x64.zip +bfbb90138b4df3f57fd9fe9cc05b6b8f9b3bf099b66d65215cc9434e2272b0d1 *mksnapshot-v30.1.2-mas-arm64.zip +d1a6a825628a141fdd73cf208180cc20af32b8e06aeb7f4d36566358cef40a90 *mksnapshot-v30.1.2-mas-x64.zip +fd523788a380990d589f9461f29a8878b63090146aa124f2debf3197af69929b *mksnapshot-v30.1.2-win32-arm64-x64.zip +69068d132cd511e33be9aff6dda5df74079c2003657cd89107d4eaaac7e4d997 *mksnapshot-v30.1.2-win32-ia32.zip +69bfab7461f83a256c0869e2781cca4f5a53f8b6a60d78617b79b8bfc0b554b6 *mksnapshot-v30.1.2-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index bcc9340406d39..877d8afe243a2 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -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 +d2148d79e9ff04d2982d00faeae942ceba488ca327a91065e528235167b9e9d6 node-v20.14.0-darwin-arm64.tar.gz +1dcc18a199cb5f46d43ed1c3c61b87a247d1a1a11dd6b32a36a9c46ac1088f86 node-v20.14.0-darwin-x64.tar.gz +d63e83fca4f81801396620c46a42892a2ef26e21a4508f68de373e61a12bd9c5 node-v20.14.0-linux-arm64.tar.gz +af45ea0d09e55a4f05c0190636532bdf9f70b2eaf0a1c4d7594207cf21284df0 node-v20.14.0-linux-armv7l.tar.gz +5b9bf40cfc7c21de14a1b4c367650e3c96eb101156bf9368ffc2f947414b6581 node-v20.14.0-linux-x64.tar.gz +a6ec02119098cf92592539e06289953c4365be20ab15d4ad264669f931000b0c win-arm64/node.exe +8f45741ec6ba07be8d199c0cebc838a58c0430c9228dbe50f8ac5d4859e58bae win-x64/node.exe diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index 7da8e55c9087b..a3daf1878b0b1 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const path = require("path"); const fs = require("fs"); +const minimatch = require("minimatch"); const vscode_universal_bundler_1 = require("vscode-universal-bundler"); const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); const root = path.dirname(path.dirname(__dirname)); @@ -18,26 +19,29 @@ async function main(buildDir) { const appName = product.nameLong + '.app'; const x64AppPath = path.join(buildDir, 'VSCode-darwin-x64', appName); const arm64AppPath = path.join(buildDir, 'VSCode-darwin-arm64', appName); - const x64AsarPath = path.join(x64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); - const arm64AsarPath = path.join(arm64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar'); const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const filesToSkip = [ + '**/CodeResources', + '**/Credits.rtf', + ]; await (0, vscode_universal_bundler_1.makeUniversalApp)({ x64AppPath, arm64AppPath, - x64AsarPath, - arm64AsarPath, - filesToSkip: [ - 'product.json', - 'Credits.rtf', - 'CodeResources', - 'fsevents.node', - 'Info.plist', // TODO@deepak1556: regressed with 11.4.2 internal builds - 'MainMenu.nib', // Generated sequence is not deterministic with Xcode 13 - '.npmrc' - ], + asarPath: asarRelativePath, outAppPath, - force: true + force: true, + mergeASARs: true, + x64ArchFiles: '*/kerberos.node', + filesToSkipComparison: (file) => { + for (const expected of filesToSkip) { + if (minimatch(file, expected)) { + return true; + } + } + return false; + } }); const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); Object.assign(productJson, { diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index ffba8952cd8e9..94b8a23b9e5b7 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import * as fs from 'fs'; +import * as minimatch from 'minimatch'; import { makeUniversalApp } from 'vscode-universal-bundler'; import { spawn } from '@malept/cross-spawn-promise'; @@ -21,27 +22,31 @@ async function main(buildDir?: string) { const appName = product.nameLong + '.app'; const x64AppPath = path.join(buildDir, 'VSCode-darwin-x64', appName); const arm64AppPath = path.join(buildDir, 'VSCode-darwin-arm64', appName); - const x64AsarPath = path.join(x64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); - const arm64AsarPath = path.join(arm64AppPath, 'Contents', 'Resources', 'app', 'node_modules.asar'); + const asarRelativePath = path.join('Contents', 'Resources', 'app', 'node_modules.asar'); const outAppPath = path.join(buildDir, `VSCode-darwin-${arch}`, appName); const productJsonPath = path.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); + const filesToSkip = [ + '**/CodeResources', + '**/Credits.rtf', + ]; + await makeUniversalApp({ x64AppPath, arm64AppPath, - x64AsarPath, - arm64AsarPath, - filesToSkip: [ - 'product.json', - 'Credits.rtf', - 'CodeResources', - 'fsevents.node', - 'Info.plist', // TODO@deepak1556: regressed with 11.4.2 internal builds - 'MainMenu.nib', // Generated sequence is not deterministic with Xcode 13 - '.npmrc' - ], + asarPath: asarRelativePath, outAppPath, - force: true + force: true, + mergeASARs: true, + x64ArchFiles: '*/kerberos.node', + filesToSkipComparison: (file: string) => { + for (const expected of filesToSkip) { + if (minimatch(file, expected)) { + return true; + } + } + return false; + } }); const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); diff --git a/build/filters.js b/build/filters.js index c7be2d818d9bf..915240f0f0bf7 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 86646fdb274ea..592fc74516c5e 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 c4947e76cbf52..de8f3c4fb5794 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 22b70a953dfb5..fe29d4fe1834a 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 559049597787e..b85425bccfc01 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 c2b81d0cf7c7e..7d58861147333 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]; @@ -125,20 +127,7 @@ function getNodeVersion() { return { nodeVersion, internalNodeVersion }; } -function getNodeChecksum(nodeVersion, platform, arch, glibcPrefix) { - let expectedName; - switch (platform) { - case 'win32': - expectedName = `win-${arch}/node.exe`; - break; - - case 'darwin': - case 'alpine': - case 'linux': - expectedName = `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`; - break; - } - +function getNodeChecksum(expectedName) { const nodeJsChecksums = fs.readFileSync(path.join(REPO_ROOT, 'build', 'checksums', 'nodejs.txt'), 'utf8'); for (const line of nodeJsChecksums.split('\n')) { const [checksum, name] = line.split(/\s+/); @@ -182,7 +171,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'; @@ -194,7 +182,24 @@ function nodejs(platform, arch) { log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from ${product.nodejsRepository}...`); const glibcPrefix = process.env['VSCODE_NODE_GLIBC'] ?? ''; - const checksumSha256 = getNodeChecksum(nodeVersion, platform, arch, glibcPrefix); + let expectedName; + switch (platform) { + case 'win32': + expectedName = product.nodejsRepository !== 'https://nodejs.org' ? + `win-${arch}-node.exe` : `win-${arch}/node.exe`; + break; + + case 'darwin': + expectedName = `node-v${nodeVersion}-${platform}-${arch}.tar.gz`; + break; + case 'linux': + expectedName = `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`; + break; + case 'alpine': + expectedName = `node-v${nodeVersion}-linux-${arch}-musl.tar.gz`; + break; + } + const checksumSha256 = getNodeChecksum(expectedName); if (checksumSha256) { log(`Using SHA256 checksum for checking integrity: ${checksumSha256}`); @@ -205,13 +210,13 @@ function nodejs(platform, arch) { switch (platform) { case 'win32': return (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: `win-${arch}-node.exe`, checksumSha256 }) : + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) : fetchUrls(`/dist/v${nodeVersion}/win-${arch}/node.exe`, { base: 'https://nodejs.org', checksumSha256 })) .pipe(rename('node.exe')); case 'darwin': case 'linux': return (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`, checksumSha256 }) : + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) : fetchUrls(`/dist/v${nodeVersion}/node-v${nodeVersion}-${platform}-${arch}.tar.gz`, { base: 'https://nodejs.org', checksumSha256 }) ).pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) .pipe(filter('**/node')) @@ -219,7 +224,7 @@ function nodejs(platform, arch) { .pipe(rename('node')); case 'alpine': return product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: `node-v${nodeVersion}-${platform}-${arch}.tar.gz`, checksumSha256 }) + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) .pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) .pipe(filter('**/node')) .pipe(util.setExecutableBit('**')) @@ -288,13 +293,22 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa } const name = product.nameShort; - const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' }) - .pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined })); - const date = new Date().toISOString(); + let packageJsonContents; + const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' }) + .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 +401,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 +438,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 +457,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 3b1aeafd080fb..4af406751f96e 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', @@ -89,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 @@ -107,17 +108,16 @@ 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', ] @@ -133,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'), @@ -249,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 }); @@ -285,6 +292,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op '**/node-pty/lib/shared/conout.js', '**/*.wasm', '**/@vscode/vsce-sign/bin/*', + ], [ + '**/*.mk', ], 'node_modules.asar')); let all = es.merge( @@ -389,6 +398,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)); }; } @@ -496,17 +511,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 8c2b62f7b2a76..28ddfb04c3d90 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; - - return shell.task([ - 'mkdir -p ' + destination, - 'HOME="$(pwd)/' + destination + '" rpmbuild -bb ' + rpmBuildPath + '/SPECS/' + product.applicationName + '.spec --target=' + rpmArch, - 'cp "' + rpmOut + '/$(ls ' + rpmOut + ')" ' + destination + '/' - ]); + const rpmOut = `${rpmBuildPath}/RPMS/${rpmArch}`; + const destination = `.build/linux/rpm/${rpmArch}`; + + 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 85129a523da67..0424b12fa45b8 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/asar.js b/build/lib/asar.js index 31845f2f2dd3f..07b39bf79ff3e 100644 --- a/build/lib/asar.js +++ b/build/lib/asar.js @@ -11,7 +11,7 @@ const pickle = require('chromium-pickle-js'); const Filesystem = require('asar/lib/filesystem'); const VinylFile = require("vinyl"); const minimatch = require("minimatch"); -function createAsar(folderPath, unpackGlobs, destFilename) { +function createAsar(folderPath, unpackGlobs, skipGlobs, destFilename) { const shouldUnpackFile = (file) => { for (let i = 0; i < unpackGlobs.length; i++) { if (minimatch(file.relative, unpackGlobs[i])) { @@ -20,6 +20,14 @@ function createAsar(folderPath, unpackGlobs, destFilename) { } return false; }; + const shouldSkipFile = (file) => { + for (const skipGlob of skipGlobs) { + if (minimatch(file.relative, skipGlob)) { + return true; + } + } + return false; + }; const filesystem = new Filesystem(folderPath); const out = []; // Keep track of pending inserts @@ -64,6 +72,9 @@ function createAsar(folderPath, unpackGlobs, destFilename) { if (!file.stat.isFile()) { throw new Error(`unknown item in stream!`); } + if (shouldSkipFile(file)) { + return; + } const shouldUnpack = shouldUnpackFile(file); insertFile(file.relative, { size: file.contents.length, mode: file.stat.mode }, shouldUnpack); if (shouldUnpack) { diff --git a/build/lib/asar.ts b/build/lib/asar.ts index 44a6416bdfbec..7dc1dd3b2e664 100644 --- a/build/lib/asar.ts +++ b/build/lib/asar.ts @@ -17,7 +17,7 @@ declare class AsarFilesystem { insertFile(path: string, shouldUnpack: boolean, file: { stat: { size: number; mode: number } }, options: {}): Promise; } -export function createAsar(folderPath: string, unpackGlobs: string[], destFilename: string): NodeJS.ReadWriteStream { +export function createAsar(folderPath: string, unpackGlobs: string[], skipGlobs: string[], destFilename: string): NodeJS.ReadWriteStream { const shouldUnpackFile = (file: VinylFile): boolean => { for (let i = 0; i < unpackGlobs.length; i++) { @@ -28,6 +28,15 @@ export function createAsar(folderPath: string, unpackGlobs: string[], destFilena return false; }; + const shouldSkipFile = (file: VinylFile): boolean => { + for (const skipGlob of skipGlobs) { + if (minimatch(file.relative, skipGlob)) { + return true; + } + } + return false; + }; + const filesystem = new Filesystem(folderPath); const out: Buffer[] = []; @@ -78,6 +87,9 @@ export function createAsar(folderPath: string, unpackGlobs: string[], destFilena if (!file.stat.isFile()) { throw new Error(`unknown item in stream!`); } + if (shouldSkipFile(file)) { + return; + } const shouldUnpack = shouldUnpackFile(file); insertFile(file.relative, { size: file.contents.length, mode: file.stat.mode }, shouldUnpack); diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 61d9f0156241b..a0989638f7cc7 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 c5fdc2da18cf4..692100ff51581 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 b44cbefe78a95..cafca34a0d834 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 b88d0d290031b..8c3614b4c137c 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 0000000000000..77fff0e5073e1 --- /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 0000000000000..998e89f8e6ab1 --- /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 8524b18850cee..99252e4e64a2d 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 ba93c3a2af39e..7a2a2a195576a 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 6a6c0a7b4cd87..58d4d3e9a7ff6 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 6edfdcb63fb12..0582e0cb11e22 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 c33994987f0d2..a837cbc4ac0c2 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')); - } - else { - this.emit('error', `Failed to read component file: ${file.relative}`); - return; + 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); + } } - 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 444e3abe59c5a..28f8cc993e642 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]; - } - 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; + + 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++; } - 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 0000000000000..f1dbfa83a7e1c --- /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 0000000000000..ef3987fc32ed1 --- /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 dce2b85d6589a..7494b71bb6651 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' - ], - 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' + // 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/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 039f222135d5f..4861fa6d86e91 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 bb6b414e845af..10e1aeb0d5acd 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 4a7544f162b53..a148c4dd637ec 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 48ca84f243317..ae235a5a534b2 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 c4ee031b2ebf4..066dc1440c2a2 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 d48235ebf15a1..79509e9a2d5ac 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 5b6dee9bf6555..6f9786b4d1e00 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 dbc47db0833fe..cf0e452aff33d 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 775a1be5996d7..9a65bfa744478 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 d8b28e0dd2481..148aa2786ddbb 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", @@ -128,15 +129,16 @@ "--vscode-dropdown-listBackground", "--vscode-editor-background", "--vscode-editor-findMatchBackground", - "--vscode-editor-findMatchForeground", "--vscode-editor-findMatchBorder", + "--vscode-editor-findMatchForeground", "--vscode-editor-findMatchHighlightBackground", - "--vscode-editor-findMatchHighlightForeground", "--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", @@ -145,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", @@ -490,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", @@ -519,6 +522,7 @@ "--vscode-problemsWarningIcon-foreground", "--vscode-profileBadge-background", "--vscode-profileBadge-foreground", + "--vscode-profiles-sashBorder", "--vscode-progressBar-background", "--vscode-quickInput-background", "--vscode-quickInput-foreground", @@ -570,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", @@ -649,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", @@ -663,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", @@ -700,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", @@ -711,6 +716,7 @@ "--vscode-terminalOverviewRuler-cursorForeground", "--vscode-terminalOverviewRuler-findMatchForeground", "--vscode-terminalStickyScroll-background", + "--vscode-terminalStickyScroll-border", "--vscode-terminalStickyScrollHover-background", "--vscode-testing-coverCountBadgeBackground", "--vscode-testing-coverCountBadgeForeground", @@ -815,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", @@ -850,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 afec9062692b7..5dcc4ca1ed389 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 a546ea63316d4..cbc3d9e8eeeb6 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/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js index d843c090063b0..3a642a7272543 100644 --- a/build/linux/debian/dep-lists.js +++ b/build/linux/debian/dep-lists.js @@ -31,6 +31,7 @@ exports.referenceGeneratedDepsByArch = { 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', 'libc6 (>= 2.2.5)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', @@ -67,6 +68,7 @@ exports.referenceGeneratedDepsByArch = { 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libc6 (>= 2.4)', 'libc6 (>= 2.9)', @@ -108,6 +110,7 @@ exports.referenceGeneratedDepsByArch = { 'libatk1.0-0 (>= 2.2.0)', 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 4028370cd02b4..86d1de1221690 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -31,6 +31,7 @@ export const referenceGeneratedDepsByArch = { 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', 'libc6 (>= 2.2.5)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', @@ -67,6 +68,7 @@ export const referenceGeneratedDepsByArch = { 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.16)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libc6 (>= 2.4)', 'libc6 (>= 2.9)', @@ -108,6 +110,7 @@ export const referenceGeneratedDepsByArch = { 'libatk1.0-0 (>= 2.2.0)', 'libatspi2.0-0 (>= 2.9.90)', 'libc6 (>= 2.17)', + 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', 'libcairo2 (>= 1.6.0)', 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index bff0c9a25df28..19adbeb052998 100644 --- a/build/linux/dependencies-generator.js +++ b/build/linux/dependencies-generator.js @@ -23,7 +23,7 @@ const product = require("../../product.json"); // The reference dependencies, which one has to update when the new dependencies // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index 226310e12581f..5fe4ac5da6430 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -25,7 +25,7 @@ import product = require('../../product.json'); // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/124.0.6367.243:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index 8be477290bbf8..97984514511f3 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -45,12 +45,14 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', 'libc.so.6(GLIBC_2.3.3)(64bit)', 'libc.so.6(GLIBC_2.3.4)(64bit)', 'libc.so.6(GLIBC_2.4)(64bit)', + 'libc.so.6(GLIBC_2.5)(64bit)', 'libc.so.6(GLIBC_2.6)(64bit)', 'libc.so.6(GLIBC_2.7)(64bit)', 'libc.so.6(GLIBC_2.8)(64bit)', @@ -140,8 +142,10 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', 'libc.so.6(GLIBC_2.18)', + 'libc.so.6(GLIBC_2.25)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', + 'libc.so.6(GLIBC_2.5)', 'libc.so.6(GLIBC_2.6)', 'libc.so.6(GLIBC_2.7)', 'libc.so.6(GLIBC_2.8)', @@ -241,6 +245,7 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 24b18d504c862..b79812784bf64 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -44,12 +44,14 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', 'libc.so.6(GLIBC_2.3.3)(64bit)', 'libc.so.6(GLIBC_2.3.4)(64bit)', 'libc.so.6(GLIBC_2.4)(64bit)', + 'libc.so.6(GLIBC_2.5)(64bit)', 'libc.so.6(GLIBC_2.6)(64bit)', 'libc.so.6(GLIBC_2.7)(64bit)', 'libc.so.6(GLIBC_2.8)(64bit)', @@ -139,8 +141,10 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', 'libc.so.6(GLIBC_2.18)', + 'libc.so.6(GLIBC_2.25)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', + 'libc.so.6(GLIBC_2.5)', 'libc.so.6(GLIBC_2.6)', 'libc.so.6(GLIBC_2.7)', 'libc.so.6(GLIBC_2.8)', @@ -240,6 +244,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', + 'libc.so.6(GLIBC_2.25)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/npm/gyp/package.json b/build/npm/gyp/package.json index 3961e955a5f84..a1564133a1eef 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 96d132e794329..a9bf901727e69 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== - -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== + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== -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" - -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" + minipass "^7.0.3" -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== - 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== +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: - 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" +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== + ssri@^10.0.0: - version "10.0.4" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.4.tgz#5a20af378be586df139ddb2dfb3bf992cf0daba6" - integrity sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ== + version "10.0.6" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" + integrity sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ== dependencies: - minipass "^5.0.0" + minipass "^7.0.3" -"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: +"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/npm/postinstall.js b/build/npm/postinstall.js index bcac781e265a7..d45d5bc8cbc94 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -54,14 +54,10 @@ function yarnInstall(dir, opts) { console.log(`Installing dependencies in ${dir} inside container ${process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME']}...`); opts.cwd = root; - if (process.env['npm_config_arch'] === 'arm64' || process.env['npm_config_arch'] === 'arm') { + if (process.env['npm_config_arch'] === 'arm64') { run('sudo', ['docker', 'run', '--rm', '--privileged', 'multiarch/qemu-user-static', '--reset', '-p', 'yes'], opts); } - if (process.env['npm_config_arch'] === 'arm') { - run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/home/builduser`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/home/builduser/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); - } else { - run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); - } + run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-e', 'npm_config_arch', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'yarn', '--cwd', dir, ...args], opts); run('sudo', ['chown', '-R', `${userinfo.uid}:${userinfo.gid}`, `${dir}/node_modules`], opts); } else { console.log(`Installing dependencies in ${dir}...`); diff --git a/build/package.json b/build/package.json index 2b89bbc1c99a2..1a73af91e6e01 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", @@ -41,10 +41,9 @@ "commander": "^7.0.0", "debug": "^4.3.2", "electron-osx-sign": "^0.4.16", - "esbuild": "0.20.0", + "esbuild": "0.23.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", @@ -52,10 +51,11 @@ "ternary-stream": "^3.0.0", "through2": "^4.0.2", "tmp": "^0.2.1", - "vscode-universal-bundler": "^0.0.2", + "vscode-universal-bundler": "^0.1.0", "workerpool": "^6.4.0", "yauzl": "^2.10.0" }, + "type": "commonjs", "scripts": { "compile": "../node_modules/.bin/tsc -p tsconfig.build.json", "watch": "../node_modules/.bin/tsc -p tsconfig.build.json --watch", diff --git a/build/yarn.lock b/build/yarn.lock index 3131c43217c20..9347576143718 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== - 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== +"@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/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" @@ -223,6 +199,15 @@ events "^3.0.0" tslib "^2.2.0" +"@electron/asar@^3.2.7": + version "3.2.10" + resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.10.tgz#615cf346b734b23cafa4e0603551010bd0e50aa8" + integrity sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw== + dependencies: + commander "^5.0.0" + glob "^7.1.6" + minimatch "^3.0.4" + "@electron/get@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@electron/get/-/get-2.0.3.tgz#fba552683d387aebd9f3fcadbcafc8e12ee4f960" @@ -238,125 +223,130 @@ optionalDependencies: global-agent "^3.0.0" -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== - -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== - -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== - -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== - -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== - -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== - -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== - -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== - -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== - -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== - -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== - -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== - -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== - -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== - -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== - -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== - -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== - -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== - -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== - -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== - -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== - -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== - -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== - -"@malept/cross-spawn-promise@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" - integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== +"@esbuild/aix-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" + integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== + +"@esbuild/android-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" + integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== + +"@esbuild/android-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" + integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== + +"@esbuild/android-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" + integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== + +"@esbuild/darwin-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" + integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== + +"@esbuild/darwin-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" + integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== + +"@esbuild/freebsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" + integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== + +"@esbuild/freebsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" + integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== + +"@esbuild/linux-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" + integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== + +"@esbuild/linux-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" + integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== + +"@esbuild/linux-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" + integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== + +"@esbuild/linux-loong64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" + integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== + +"@esbuild/linux-mips64el@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" + integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== + +"@esbuild/linux-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" + integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== + +"@esbuild/linux-riscv64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" + integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== + +"@esbuild/linux-s390x@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" + integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== + +"@esbuild/linux-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" + integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== + +"@esbuild/netbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" + integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== + +"@esbuild/openbsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" + integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== + +"@esbuild/openbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" + integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== + +"@esbuild/sunos-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" + integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== + +"@esbuild/win32-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" + integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== + +"@esbuild/win32-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" + integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== + +"@esbuild/win32-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" + integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== + +"@malept/cross-spawn-promise@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz#d0772de1aa680a0bfb9ba2f32b4c828c7857cb9d" + integrity sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg== dependencies: cross-spawn "^7.0.1" @@ -710,6 +700,11 @@ optionalDependencies: keytar "^7.7.0" +"@xmldom/xmldom@^0.8.8": + version "0.8.10" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" + integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -743,13 +738,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" @@ -786,18 +774,6 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -asar@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/asar/-/asar-3.0.3.tgz#1fef03c2d6d2de0cbad138788e4f7ae03b129c7b" - integrity sha512-k7zd+KoR+n8pl71PvgElcoKHrVNiSXtw7odKbyNpmgKe7EGRF9Pnu3uLOukD37EvavKwVFxOUpqXTIZC5B5Pmw== - dependencies: - chromium-pickle-js "^0.2.0" - commander "^5.0.0" - glob "^7.1.6" - minimatch "^3.0.4" - optionalDependencies: - "@types/glob" "^7.1.1" - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -818,11 +794,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - azure-devops-node-api@^11.0.1: version "11.2.0" resolved "https://registry.yarnpkg.com/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz#bf04edbef60313117a0507415eed4790a420ad6b" @@ -878,12 +849,19 @@ brace-expansion@^1.1.7: 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" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.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-alloc-unsafe@^1.1.0: version "1.1.0" @@ -908,11 +886,6 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= -buffer-equal@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" - integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= - buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -966,14 +939,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" @@ -1034,11 +999,6 @@ chownr@^1.1.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== -chromium-pickle-js@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" - integrity sha1-BKEGZywYsIWrd02YPfo+oTjyIgU= - clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -1077,33 +1037,16 @@ 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" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -colors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= - colors@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -1116,13 +1059,6 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= - dependencies: - graceful-readlink ">= 1.0.0" - commander@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" @@ -1236,15 +1172,13 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -dir-compare@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-2.4.0.tgz#785c41dc5f645b34343a4eafc50b79bac7f11631" - integrity sha512-l9hmu8x/rjVC9Z2zmGzkhOEowZvW7pmYws5CWHutg8u1JgvsKWMx7Q/UODeu4djLZ4FgW5besw5yvMQnBHzuCA== +dir-compare@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-4.2.0.tgz#d1d4999c14fbf55281071fdae4293b3b9ce86f19" + integrity sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ== dependencies: - buffer-equal "1.0.0" - colors "1.0.3" - commander "2.9.0" - minimatch "3.0.4" + minimatch "^3.0.5" + p-limit "^3.1.0 " dom-serializer@^2.0.0: version "2.0.0" @@ -1332,34 +1266,35 @@ es6-error@^4.1.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -esbuild@0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== +esbuild@0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599" + integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" + "@esbuild/aix-ppc64" "0.23.0" + "@esbuild/android-arm" "0.23.0" + "@esbuild/android-arm64" "0.23.0" + "@esbuild/android-x64" "0.23.0" + "@esbuild/darwin-arm64" "0.23.0" + "@esbuild/darwin-x64" "0.23.0" + "@esbuild/freebsd-arm64" "0.23.0" + "@esbuild/freebsd-x64" "0.23.0" + "@esbuild/linux-arm" "0.23.0" + "@esbuild/linux-arm64" "0.23.0" + "@esbuild/linux-ia32" "0.23.0" + "@esbuild/linux-loong64" "0.23.0" + "@esbuild/linux-mips64el" "0.23.0" + "@esbuild/linux-ppc64" "0.23.0" + "@esbuild/linux-riscv64" "0.23.0" + "@esbuild/linux-s390x" "0.23.0" + "@esbuild/linux-x64" "0.23.0" + "@esbuild/netbsd-x64" "0.23.0" + "@esbuild/openbsd-arm64" "0.23.0" + "@esbuild/openbsd-x64" "0.23.0" + "@esbuild/sunos-x64" "0.23.0" + "@esbuild/win32-arm64" "0.23.0" + "@esbuild/win32-ia32" "0.23.0" + "@esbuild/win32-x64" "0.23.0" escape-string-regexp@^1.0.5: version "1.0.5" @@ -1422,10 +1357,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" @@ -1464,6 +1399,15 @@ 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.1.1: + 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" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -1473,16 +1417,6 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1601,11 +1535,6 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= - gulp-merge-json@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/gulp-merge-json/-/gulp-merge-json-2.1.1.tgz#cfb1d066467577545b8c1c289a278e6ef4b4e0de" @@ -1617,28 +1546,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 +1829,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" @@ -2014,19 +1906,26 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@^3.0.3, minimatch@^3.0.5, 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@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.3, 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== +minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.1" minimist@^1.2.0, minimist@^1.2.3: version "1.2.6" @@ -2150,6 +2049,13 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== +"p-limit@^3.1.0 ": + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + parse-node-version@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" @@ -2215,6 +2121,15 @@ plist@^3.0.1: base64-js "^1.5.1" xmlbuilder "^9.0.7" +plist@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" + integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== + dependencies: + "@xmldom/xmldom" "^0.8.8" + base64-js "^1.5.1" + xmlbuilder "^15.1.1" + plugin-error@1.0.1, plugin-error@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" @@ -2559,13 +2474,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 +2565,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" @@ -2783,16 +2681,18 @@ vscode-gulp-watch@^5.0.3: vinyl "^2.2.0" vinyl-file "^3.0.0" -vscode-universal-bundler@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/vscode-universal-bundler/-/vscode-universal-bundler-0.0.2.tgz#2c988dac681d3ffe6baec6defac0995cb833c55a" - integrity sha512-FPJcvKnQGBqFzy6M6Nm2yvAczNLUeXsfYM6GwCex/pUOkvIM2icIHmiSvtMJINlLW1iG+oEwE3/LVbABmcjEmQ== +vscode-universal-bundler@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/vscode-universal-bundler/-/vscode-universal-bundler-0.1.0.tgz#1a03d1d16c6ea5065318fafbc2a554b7c2f3bd32" + integrity sha512-wtT9IZ/fqIZSirY6cxElu8a6WpNaOjgQjjazt85lMCWBuF/tWVw5nRYX67pTVsUyi6kzQaIvfyyIvxVbBpetBA== dependencies: - "@malept/cross-spawn-promise" "^1.1.0" - asar "^3.0.3" + "@electron/asar" "^3.2.7" + "@malept/cross-spawn-promise" "^2.0.0" debug "^4.3.1" - dir-compare "^2.4.0" - fs-extra "^9.0.1" + dir-compare "^4.2.0" + fs-extra "^11.1.1" + minimatch "^9.0.3" + plist "^3.1.0" webidl-conversions@^3.0.0: version "3.0.1" @@ -2832,6 +2732,11 @@ xml2js@^0.4.19, xml2js@^0.4.23: sax ">=0.6.0" xmlbuilder "~11.0.0" +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + xmlbuilder@^9.0.7: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" @@ -2861,3 +2766,8 @@ yazl@^2.2.2: integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== dependencies: buffer-crc32 "~0.2.3" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/cgmanifest.json b/cgmanifest.json index 61747342eef4b..aa06f8323b09e 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "f1a45d7ded05d64ca8136cc142ddc0c271b1dd43" + "commitHash": "7fa4f6e14e0707c0e604cf7c1da33566e78169ce" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "122.0.6261.156" + "version": "124.0.6367.243" }, { "component": { @@ -48,7 +48,7 @@ "git": { "name": "ffmpeg", "repositoryUrl": "https://chromium.googlesource.com/chromium/third_party/ffmpeg", - "commitHash": "17525de887d54b970ffdd421a0879c1db1952307" + "commitHash": "52d8ef3799b2f16b66351dd0972bb0bcee1648ac" } }, "isOnlyProductionDependency": true, @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "9b1bf44ea9e7785e38c93b7d22d32dbca262df6c" + "commitHash": "fe0f08a5dd68fd72b1652adaa51ab07a4b09f847" } }, "isOnlyProductionDependency": true, - "version": "20.11.1" + "version": "20.14.0" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "f9ed0eaee4b172733872c2f84e5061882dd08e5c" + "commitHash": "91de7d0f13208891c5604e00ccd18e4f6826653b" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "29.4.0" + "version": "30.1.2" }, { "component": { diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 7a74fb503c518..a9630c120224a 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -1341,7 +1341,7 @@ SOFTWARE. --------------------------------------------------------- clap_derive 4.5.4 - MIT OR Apache-2.0 -https://github.com/clap-rs/clap/tree/master/clap_derive +https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -1367,7 +1367,7 @@ SOFTWARE. --------------------------------------------------------- clap_lex 0.7.0 - MIT OR Apache-2.0 -https://github.com/clap-rs/clap/tree/master/clap_lex +https://github.com/clap-rs/clap Copyright (c) Individual contributors @@ -3428,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 @@ -8507,6 +8507,7 @@ 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 @@ -10790,33 +10791,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI 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 --------------------------------------------------------- --------------------------------------------------------- @@ -10824,33 +10799,7 @@ DEALINGS IN THE SOFTWARE. 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 --------------------------------------------------------- --------------------------------------------------------- @@ -10858,33 +10807,7 @@ DEALINGS IN THE SOFTWARE. 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 --------------------------------------------------------- --------------------------------------------------------- @@ -10977,33 +10900,7 @@ licences; see files named LICENSE.*.txt for details. 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 --------------------------------------------------------- --------------------------------------------------------- @@ -11011,33 +10908,7 @@ DEALINGS IN THE SOFTWARE. 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 --------------------------------------------------------- --------------------------------------------------------- @@ -11045,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 e9b710c1d6815..78aed6fe3e769 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 67f1bfa6bc7ae..2d9162c548321 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 79c4d3767a157..05e22e0cfb372 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 fba927234260a..12c0cdafec908 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/constants.rs b/cli/src/constants.rs index 6f604e8876ee6..1e277a89d6af2 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 f42984cfac150..dfb5e381179f2 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/util/prereqs.rs b/cli/src/util/prereqs.rs index 5d4953acbb208..0f49ab20887c9 100644 --- a/cli/src/util/prereqs.rs +++ b/cli/src/util/prereqs.rs @@ -19,12 +19,22 @@ lazy_static! { static ref GENERIC_VERSION_RE: Regex = Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap(); static ref LIBSTD_CXX_VERSION_RE: BinRegex = BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap(); - static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 25); - static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19); static ref MIN_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 28, 0); static ref MIN_LEGACY_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 17, 0); } +#[cfg(target_arch = "arm")] +lazy_static! { + static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 26); + static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 22); +} + +#[cfg(not(target_arch = "arm"))] +lazy_static! { + static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 25); + static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19); +} + const NIXOS_TEST_PATH: &str = "/etc/NIXOS"; pub struct PreReqChecker {} diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 8a00fda49b88a..f6bfd75189576 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 c1d3f4882f628..9f3c890a48bbd 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 1c88bd17296ca..58a7408dbbe7d 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 96dbe04473c32..4a2497a064abe 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 6522c78638988..c89997ffaa041 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 573b24b4aa636..44b77895c10b7 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 f4f6adfb7f41c..4ddfe8fce0def 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 0f1750e800acb..fe4f64d7c0138 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 8d4c46d641e7a..59033f770c16b 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 25a22d07ca662..eef1c9ab57d39 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 dd1727edb7b0a..b69dac0e2dd35 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 524b3fa0d46a4..0b3c5d112e57d 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 5063f1c5210c0..7806c100eae96 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 dfbb29289db50..ccac6222db925 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -16,6 +16,7 @@ "contribMergeEditorMenus", "contribMultiDiffEditorMenus", "contribDiffEditorGutterToolBarMenus", + "contribSourceControlHistoryItemChangesMenu", "contribSourceControlHistoryItemGroupMenu", "contribSourceControlHistoryItemMenu", "contribSourceControlInputBoxMenu", @@ -1915,6 +1916,28 @@ "group": "1_modification@3" } ], + "scm/historyItemChanges/title": [ + { + "command": "git.fetchRef", + "group": "navigation@1", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote" + }, + { + "command": "git.pullRef", + "group": "navigation@2", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote" + }, + { + "command": "git.pushRef", + "when": "scmProvider == git && scmHistoryItemGroupHasRemote", + "group": "navigation@3" + }, + { + "command": "git.publish", + "when": "scmProvider == git && !scmHistoryItemGroupHasRemote", + "group": "navigation@3" + } + ], "scm/incomingChanges": [ { "command": "git.fetchRef", @@ -1967,23 +1990,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 +3424,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 f049939c137f6..f94ecbab7b0e0 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 685b541394700..ce27e91424441 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 48f67f396d4b2..dc391ce61db3c 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,74 +2063,79 @@ 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 (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) { - documents = documents - .filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, 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 (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) { - 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 + // 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; + } } - } - if (opts.all === undefined) { - opts = { ...opts, all: noStagedChanges }; - } else if (!opts.all && noStagedChanges && !opts.empty) { - opts = { ...opts, all: true }; + // smart commit + if (enableSmartCommit && !opts.all) { + opts = { ...opts, all: noStagedChanges }; + } } // enable signing of commits if configured @@ -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); @@ -3044,7 +3051,8 @@ export class CommandCenter { } @command('git.fetchRef', { repository: true }) - async fetchRef(repository: Repository, ref: string): Promise { + async fetchRef(repository: Repository, ref?: string): Promise { + ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; if (!repository || !ref) { return; } @@ -3116,7 +3124,8 @@ export class CommandCenter { } @command('git.pullRef', { repository: true }) - async pullRef(repository: Repository, ref: string): Promise { + async pullRef(repository: Repository, ref?: string): Promise { + ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; if (!repository || !ref) { return; } @@ -3263,8 +3272,8 @@ export class CommandCenter { } @command('git.pushRef', { repository: true }) - async pushRef(repository: Repository, ref: string): Promise { - if (!repository || !ref) { + async pushRef(repository: Repository): Promise { + if (!repository) { return; } @@ -4400,10 +4409,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 3f8553260e9c6..ace68c2252453 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 a283f628594ed..c80fb6ee6d5e6 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 697e77815e4b2..29b4cccb82d00 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}.`)); } @@ -2657,7 +2670,7 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.exec(['show', '-s', `--format=${COMMIT_FORMAT}`, '-z', ref]); + const result = await this.exec(['show', '-s', '--decorate=full', `--format=${COMMIT_FORMAT}`, '-z', ref]); const commits = parseGitCommits(result.stdout); if (commits.length === 0) { return Promise.reject('bad commit format'); @@ -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 f238010e14c22..b7ebb00e9806e 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,60 @@ 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 []; + } + + const historyItems: SourceControlHistoryItem[] = []; + + try { + // Get the common ancestor commit, and commits + const [mergeBaseCommit, commits] = await Promise.all([ + this.repository.getCommit(refsMergeBase), + this.repository.log({ range: `${refsMergeBase}..`, refNames, shortStats: true }) + ]); + + // Add common ancestor commit + if (commits.length !== 0) { + commits.push(mergeBaseCommit); + } + + await ensureEmojis(); + + 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 + }; + })); + } catch (err) { + this.logger.error(`[GitHistoryProvider][provideHistoryItems2] Failed to get history items '${refsMergeBase}..': ${err}`); + } + + return historyItems; + } + async provideHistoryItemSummary(historyItemId: string, historyItemParentId: string | undefined): Promise { if (!historyItemParentId) { const commit = await this.repository.getCommit(historyItemId); @@ -161,9 +234,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 +245,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 +264,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 +306,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 +317,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 c2d9b974be726..b3f6b6466fe2b 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; - } + 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}`); + 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', []); + // 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)); + 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(', ')}]`); + // 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('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.'); - continue; - } + 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(notSupportedMessage); - console.warn(notSupportedMessage); - 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)); - } + subfolders.add(path.join(root, scanPath)); + } - this.logger.trace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`); - await Promise.all([...subfolders].map(f => this.openRepository(f))); - })); + 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 { @@ -388,7 +397,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } } catch (err) { - this.logger.warn(`[swsf] Unable to read folder '${currentFolder.path}': ${err}`); + this.logger.warn(`[Model][traverseWorkspaceFolder] Unable to read workspace folder '${currentFolder.path}': ${err}`); continue; } @@ -434,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)); - - 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[]; - - 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))); + 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 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(`[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 { @@ -463,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; } @@ -514,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; } @@ -524,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 { @@ -534,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; } @@ -552,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 @@ -569,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)) { @@ -583,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; } @@ -594,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}`); } } @@ -631,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; } } @@ -658,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()); @@ -675,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; } @@ -688,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); }); }; @@ -750,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(); @@ -803,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; } } @@ -989,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 223f1945b0214..13370d59bf739 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 dc73fe39965c2..90491fecd5067 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 ed959765a5928..5c14345e61a6b 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 266157e9e5c91..a7e1a693f84dc 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/github-authentication/package.json b/extensions/github-authentication/package.json index 2d2bea562771b..ed024ee59b8f0 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -13,6 +13,9 @@ "Other" ], "api": "none", + "enabledApiProposals": [ + "authGetSessions" + ], "extensionKind": [ "ui", "workspace" diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 7498a2b22025a..a2497b2b0b2e7 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -173,6 +173,8 @@ const allFlows: IFlow[] = [ ]); if (existingLogin) { searchParams.append('login', existingLogin); + } else { + searchParams.append('prompt', 'select_account'); } // The extra toString, parse is apparently needed for env.openExternal @@ -240,6 +242,8 @@ const allFlows: IFlow[] = [ ]); if (existingLogin) { searchParams.append('login', existingLogin); + } else { + searchParams.append('prompt', 'select_account'); } const loginUrl = baseUri.with({ diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 15fe2ef04f894..08fb16730e6b4 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -11,7 +11,7 @@ import { PromiseAdapter, arrayEquals, promiseFromEvent } from './common/utils'; import { ExperimentationTelemetry } from './common/experimentationService'; import { Log } from './common/logger'; import { crypto } from './node/crypto'; -import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; interface SessionData { id: string; @@ -148,14 +148,17 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid return this._sessionChangeEmitter.event; } - async getSessions(scopes?: string[]): Promise { + async getSessions(scopes: string[] | undefined, options?: vscode.AuthenticationProviderSessionOptions): Promise { // For GitHub scope list, order doesn't matter so we immediately sort the scopes const sortedScopes = scopes?.sort() || []; this._logger.info(`Getting sessions for ${sortedScopes.length ? sortedScopes.join(',') : 'all scopes'}...`); const sessions = await this._sessionsPromise; - const finalSessions = sortedScopes.length - ? sessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes)) + const accountFilteredSessions = options?.account + ? sessions.filter(session => session.account.label === options.account?.label) : sessions; + const finalSessions = sortedScopes.length + ? accountFilteredSessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes)) + : accountFilteredSessions; this._logger.info(`Got ${finalSessions.length} sessions for ${sortedScopes?.join(',') ?? 'all scopes'}...`); return finalSessions; @@ -279,7 +282,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid this._logger.info(`Stored ${sessions.length} sessions!`); } - public async createSession(scopes: string[]): Promise { + public async createSession(scopes: string[], options?: vscode.AuthenticationProviderSessionOptions): Promise { try { // For GitHub scope list, order doesn't matter so we use a sorted scope to determine // if we've got a session already. @@ -298,11 +301,59 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid const sessions = await this._sessionsPromise; - const accounts = new Set(sessions.map(session => session.account.label)); - const existingLogin = accounts.size <= 1 ? sessions[0]?.account.label : await vscode.window.showQuickPick([...accounts], { placeHolder: 'Choose an account that you would like to log in to' }); + let forcedLogin = options?.account?.label; + let backupLogin: string | undefined; + if (!forcedLogin) { + const accounts = new Set(sessions.map(session => session.account.label)); + this._logger.info(`Found ${accounts.size} accounts.`); + // This helps us tell GitHub that we're already logged in to an account/accounts + // and should probably use it. The user _can_ sign in to a different account + // if they want to, in the browser, but this is a good default for the happy path. + if (accounts.size > 1) { + // If there are multiple accounts, we should prompt the user to choose one. + const newAccount = vscode.l10n.t('New account...'); + const accountChoiceResult = await vscode.window.showQuickPick( + [...accounts, newAccount], + { placeHolder: vscode.l10n.t('Choose an account that you would like to log in to') } + ); + forcedLogin = accountChoiceResult === newAccount ? undefined : accountChoiceResult; + } else { + // If there is only one account, we can use that to seed the login, but + // we don't want to force the user to use it. + backupLogin = sessions[0]?.account.label; + } + } + this._logger.info(`Logging in with '${forcedLogin ? forcedLogin : 'any'}' account...`); + const scopeString = sortedScopes.join(' '); - const token = await this._githubServer.login(scopeString, existingLogin); + const token = await this._githubServer.login(scopeString, forcedLogin ?? backupLogin); const session = await this.tokenToSession(token, scopes); + + // If an account was specified, we should ensure that the token we got back is for that account. + if (forcedLogin) { + if (session.account.label !== forcedLogin) { + const keepNewAccount = vscode.l10n.t('Keep {0}', session.account.label); + const tryAgain = vscode.l10n.t('Login with {0}', forcedLogin); + const result = await vscode.window.showWarningMessage( + vscode.l10n.t('Incorrect account detected'), + { modal: true, detail: vscode.l10n.t('The chosen account, {0}, does not match the requested account, {1}.', session.account.label, forcedLogin) }, + keepNewAccount, + tryAgain + ); + if (result === tryAgain) { + return await this.createSession(scopes, { + ...options, + // The id doesn't matter here, we just need to pass the label through + account: { id: forcedLogin, label: forcedLogin } + }); + } + // Cancelled result + if (!result) { + throw new Error(CANCELLATION_ERROR); + } + // Keep result continues on + } + } this.afterSessionLoad(session); const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); diff --git a/extensions/github-authentication/tsconfig.json b/extensions/github-authentication/tsconfig.json index 5e4713e9f3bc5..70eba984a2694 100644 --- a/extensions/github-authentication/tsconfig.json +++ b/extensions/github-authentication/tsconfig.json @@ -12,6 +12,7 @@ }, "include": [ "src/**/*", - "../../src/vscode-dts/vscode.d.ts" + "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.authGetSessions.d.ts" ] } diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index d27352e133961..bd8f2d6105f02 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": "092c45ec9a51fe40188408d1371f123eaa4796fa" + "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.8" + "version": "0.7.5" } ], "version": 1 diff --git a/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index 21b370514d0a9..b8a6604de88b1 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/092c45ec9a51fe40188408d1371f123eaa4796fa", + "version": "https://github.com/worlpaker/go-syntax/commit/21f28840e04d4fa04682d19d6fe64de437f40b64", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -321,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" @@ -1862,7 +1862,7 @@ }, { "comment": "property variables and types", - "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))([\\s\\S]+))", + "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))([^\\`]+))", "captures": { "1": { "patterns": [ @@ -2007,6 +2007,29 @@ } ] }, + { + "comment": "one type only with multi line raw string", + "begin": "(?:((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?\\|\\&]+))(\\.\\(\\btype\\b\\)\\s*)(\\{)", "beginCaptures": { "1": { "patterns": [ @@ -2770,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": [ @@ -2786,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": [ @@ -2799,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" - } - ] } } }, @@ -2931,13 +2895,16 @@ }, "2": { "patterns": [ - { - "include": "#type-declarations" - }, { "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" diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts index 3f10e6d131fa7..06997d39fb0af 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 8f5cef74fd3a3..349af163eea53 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 49489ff20df44..ac026b973eb18 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 75bfa00de1107..c1ddc242fa4a3 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 f327f1f352f55..caaf929d89547 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 d1d73407809c6..aa2ea1c684073 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 d923904ef9d3f..d881eb8ca2216 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 af7d8f4ab4717..7a3d95181cfcd 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 889f4c074453f..6d73107ef54e8 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/snippets/javascript.code-snippets b/extensions/javascript/snippets/javascript.code-snippets index 9448fd140d930..5bf6aa5edeeaa 100644 --- a/extensions/javascript/snippets/javascript.code-snippets +++ b/extensions/javascript/snippets/javascript.code-snippets @@ -1,16 +1,79 @@ { - "define module": { - "prefix": "define", - "body": [ - "define([", - "\t'require',", - "\t'${1:dependency}'", - "], function(require, ${2:factory}) {", - "\t'use strict';", + "Constructor": { + "prefix": "ctor", + "body": [ + "/**", + " *", + " */", + "constructor() {", + "\tsuper();", "\t$0", - "});" + "}" + ], + "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": "define module" + "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,34 +235,31 @@ ], "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", + "new Promise": { + "prefix": "newpromise", "body": [ - "console.warn($1);" + "new Promise((resolve, reject) => {", + "\t$TM_SELECTED_TEXT$0", + "})" ], - "description": "Log warning to the console" + "description": "Create a new Promise" }, - "Log error to console": { - "prefix": "error", + "Async Function Statement": { + "prefix": "async function", "body": [ - "console.error($1);" + "async function ${1:name}(${2:params}) {", + "\t$TM_SELECTED_TEXT$0", + "}" ], - "description": "Log error to the console" + "description": "Async Function Statement" }, - "new Promise": { - "prefix": "newpromise", + "Async Function Expression": { + "prefix": "async arrow function", "body": [ - "new Promise((resolve, reject) => {", + "async (${1:params}) => {", "\t$TM_SELECTED_TEXT$0", - "})" + "}" ], - "description": "Create a new Promise" + "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 f78f494d72713..91ed937fe6fad 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/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index f892664d9170f..90aafc89b8444 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -8,7 +8,8 @@ export type JSONLanguageStatus = { schemas: string[] }; import { workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, - ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n + ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n, + RelativePattern } from 'vscode'; import { LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, DocumentDiagnosticReportKind, @@ -360,18 +361,29 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa const schemaDocuments: { [uri: string]: boolean } = {}; // handle content request - client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { + client.onRequest(VSCodeContentRequest.type, async (uriPath: string) => { const uri = Uri.parse(uriPath); + const uriString = uri.toString(); if (uri.scheme === 'untitled') { - return Promise.reject(new ResponseError(3, l10n.t('Unable to load {0}', uri.toString()))); + throw new ResponseError(3, l10n.t('Unable to load {0}', uriString)); } - if (uri.scheme !== 'http' && uri.scheme !== 'https') { - return workspace.openTextDocument(uri).then(doc => { - schemaDocuments[uri.toString()] = true; - return doc.getText(); - }, error => { - return Promise.reject(new ResponseError(2, error.toString())); - }); + if (uri.scheme === 'vscode') { + try { + runtime.logOutputChannel.info('read schema from vscode: ' + uriString); + ensureFilesystemWatcherInstalled(uri); + const content = await workspace.fs.readFile(uri); + return new TextDecoder().decode(content); + } catch (e) { + throw new ResponseError(5, e.toString(), e); + } + } else if (uri.scheme !== 'http' && uri.scheme !== 'https') { + try { + const document = await workspace.openTextDocument(uri); + schemaDocuments[uriString] = true; + return document.getText(); + } catch (e) { + throw new ResponseError(2, e.toString(), e); + } } else if (schemaDownloadEnabled) { if (runtime.telemetry && uri.authority === 'schema.management.azure.com') { /* __GDPR__ @@ -381,13 +393,15 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa "schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The azure schema URL that was requested." } } */ - runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriPath }); + runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriString }); + } + try { + return await runtime.schemaRequests.getContent(uriString); + } catch (e) { + throw new ResponseError(4, e.toString()); } - return runtime.schemaRequests.getContent(uriPath).catch(e => { - return Promise.reject(new ResponseError(4, e.toString())); - }); } else { - return Promise.reject(new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload))); + throw new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); } }); @@ -415,15 +429,50 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa schemaResolutionErrorStatusBarItem.hide(); } }; - - toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); - toDispose.push(workspace.onDidCloseTextDocument(d => { - const uriString = d.uri.toString(); + const handleContentClosed = (uriString: string) => { if (handleContentChange(uriString)) { delete schemaDocuments[uriString]; } fileSchemaErrors.delete(uriString); + }; + + const watchers: Map = new Map(); + toDispose.push(new Disposable(() => { + for (const d of watchers.values()) { + d.dispose(); + } })); + + + const ensureFilesystemWatcherInstalled = (uri: Uri) => { + + const uriString = uri.toString(); + if (!watchers.has(uriString)) { + try { + const watcher = workspace.createFileSystemWatcher(new RelativePattern(uri, '*')); + const handleChange = (uri: Uri) => { + runtime.logOutputChannel.info('schema change detected ' + uri.toString()); + client.sendNotification(SchemaContentChangeNotification.type, uriString); + }; + const createListener = watcher.onDidCreate(handleChange); + const changeListener = watcher.onDidChange(handleChange); + const deleteListener = watcher.onDidDelete(() => { + const watcher = watchers.get(uriString); + if (watcher) { + watcher.dispose(); + watchers.delete(uriString); + } + }); + watchers.set(uriString, Disposable.from(watcher, createListener, changeListener, deleteListener)); + } catch { + runtime.logOutputChannel.info('Problem installing a file system watcher for ' + uriString); + } + } + }; + + toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); + toDispose.push(workspace.onDidCloseTextDocument(d => handleContentClosed(d.uri.toString()))); + toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); const handleRetryResolveSchemaCommand = () => { diff --git a/extensions/json-language-features/client/tsconfig.json b/extensions/json-language-features/client/tsconfig.json index aa51e4d0157e1..89e6a6c12b7e4 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 f86470429a4c0..fa8004e2b02ff 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 6134fb4224d0c..8472ca618a4fd 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 669e823497d03..608619637e412 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 b7ca937103a52..c825de0768334 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/syntaxes/LaTeX.tmLanguage.json b/extensions/latex/syntaxes/LaTeX.tmLanguage.json index aad6c5c4bd9f2..7648673219530 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/less/cgmanifest.json b/extensions/less/cgmanifest.json index caf908bbcc05d..a8d1702aa82eb 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 2acac68838544..cea782810fbe5 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/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 60c6b192bed6a..380b0c74ac6df 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "f75d5f55730e72ee7ff386841949048b2395e440" + "commitHash": "7418dd20d76c72e82fadee2909e03239e9973b35" } }, "license": "MIT", diff --git a/extensions/markdown-basics/language-configuration.json b/extensions/markdown-basics/language-configuration.json index f1e7859ccca57..6e1766db02cdc 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-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index c84c468b80c8a..9761ca716abde 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/f75d5f55730e72ee7ff386841949048b2395e440", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/7418dd20d76c72e82fadee2909e03239e9973b35", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -2480,14 +2480,34 @@ "name": "meta.separator.markdown" }, "frontMatter": { - "begin": "\\A-{3}\\s*$", - "contentName": "meta.embedded.block.frontmatter", + "begin": "\\A(?=(-{3,}))", + "end": "^ {,3}\\1-*[ \\t]*$|^[ \\t]*\\.{3}$", + "applyEndPatternLast": 1, + "endCaptures": { + "0": { + "name": "punctuation.definition.end.frontmatter" + } + }, "patterns": [ { - "include": "source.yaml" + "begin": "\\A(-{3,})(.*)$", + "while": "^(?! {,3}\\1-*[ \\t]*$|[ \\t]*\\.{3}$)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.begin.frontmatter" + }, + "2": { + "name": "comment.frontmatter" + } + }, + "contentName": "meta.embedded.block.frontmatter", + "patterns": [ + { + "include": "source.yaml" + } + ] } - ], - "end": "(^|\\G)-{3}|\\.{3}\\s*$" + ] }, "table": { "name": "markup.table.markdown", diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index 168f6a8a862b4..800be985a43af 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 3d73a7621fde3..fd3ba0770281e 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 df36686dc9a3d..bc4d71e56d610 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 02cfb4643f419..87dc94e4c2545 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 4b9d06d1847ef..cad76d078bd8a 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 8954017fffc54..8f5fa908cb938 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 b49dbb6ad8d08..9c080c7f9e42f 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 3cbe531e0fd2e..00c3e704dba1f 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 a7afc9f801f53..be4b192c67d58 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 2c83af40936a7..82b6c190ad802 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,14 +4,14 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "5.4.5" + "typescript": "^5.5.2" }, "scripts": { "postinstall": "node ./postinstall.mjs" }, "devDependencies": { "@parcel/watcher": "2.1.0", - "esbuild": "0.20.0", + "esbuild": "0.23.0", "vscode-grammar-updater": "^1.1.0" }, "resolutions": { diff --git a/extensions/python/cgmanifest.json b/extensions/python/cgmanifest.json index 37a21b2de54cb..ace7056c99523 100644 --- a/extensions/python/cgmanifest.json +++ b/extensions/python/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "MagicStack/MagicPython", "repositoryUrl": "https://github.com/MagicStack/MagicPython", - "commitHash": "c9b3409deb69acec31bbf7913830e93a046b30cc" + "commitHash": "7d0f2b22a5ad8fccbd7341bc7b7a715169283044" } }, "license": "MIT", diff --git a/extensions/swift/cgmanifest.json b/extensions/swift/cgmanifest.json index 816621e4170ee..cb1ca02310f9f 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 6259b151369fb..b18b340f2c686 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/typescript-basics/snippets/typescript.code-snippets b/extensions/typescript-basics/snippets/typescript.code-snippets index 35b2aa1711cf8..9ed695795eb6d 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 4c8a54d8f3956..3f7fdcf252f3d 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -1315,15 +1315,21 @@ "markdownDescription": "%typescript.workspaceSymbols.excludeLibrarySymbols%", "scope": "window" }, + "typescript.tsserver.enableRegionDiagnostics": { + "type": "boolean", + "default": true, + "description": "%typescript.tsserver.enableRegionDiagnostics%", + "scope": "window" + }, "javascript.experimental.updateImportsOnPaste": { - "scope": "resource", + "scope": "window", "type": "boolean", "default": false, "description": "%configuration.updateImportsOnPaste%", "tags": ["experimental"] }, "typescript.experimental.updateImportsOnPaste": { - "scope": "resource", + "scope": "window", "type": "boolean", "default": false, "description": "%configuration.updateImportsOnPaste%", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 4f276905d967a..cba24007314c4 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,7 +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.5+.", + "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 a08ca921e0ce6..639f3d346e0ad 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -124,6 +124,7 @@ export interface TypeScriptServiceConfiguration { 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), }; } @@ -267,4 +269,8 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu private readWebTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean { 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/languageFeatures/copyPaste.ts b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts index 643c77ac35767..83a7bb38639cc 100644 --- a/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts +++ b/extensions/typescript-language-features/src/languageFeatures/copyPaste.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { DocumentSelector } from '../configuration/documentSelector'; import * as typeConverters from '../typeConverters'; import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; -import { conditionalRegistration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; +import { conditionalRegistration, requireGlobalConfiguration, requireMinVersion, requireSomeCapability } from './util/dependentRegistration'; import protocol from '../tsServer/protocol/protocol'; import { API } from '../tsServer/api'; import { LanguageDescription } from '../configuration/languageDescription'; @@ -38,6 +38,8 @@ class CopyMetadata { } } +const settingId = 'experimental.updateImportsOnPaste'; + class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('text', 'jsts', 'pasteWithImports'); @@ -61,7 +63,7 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { token: vscode.CancellationToken, ): Promise { const config = vscode.workspace.getConfiguration(this._modeId, document.uri); - if (!config.get('experimental.updateImportsOnPaste')) { + if (!config.get(settingId, false)) { return; } @@ -93,13 +95,17 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { } } - const response = await this._client.execute('getPasteEdits', { + 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); + }, token)); if (response.type !== 'response' || !response.body || token.isCancellationRequested) { return; } @@ -126,7 +132,8 @@ class DocumentPasteProvider implements vscode.DocumentPasteEditProvider { export function register(selector: DocumentSelector, language: LanguageDescription, client: ITypeScriptServiceClient) { return conditionalRegistration([ requireSomeCapability(client, ClientCapability.Semantic), - requireMinVersion(client, API.v550), + requireMinVersion(client, API.v560), + requireGlobalConfiguration(language.id, settingId), ], () => { return vscode.languages.registerDocumentPasteEditProvider(selector.semantic, new DocumentPasteProvider(language.id, client), { providedPasteEditKinds: [DocumentPasteProvider.kind], diff --git a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts index 190e6a99bf789..990aefdfa56ae 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 f96f89452b9a5..bddd062b3e8bb 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 8dc062357533e..0364b7fad3ef9 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/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index a192740986918..7b95591604bd0 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -138,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; } @@ -175,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 4f26db47513a4..4beb29d1b2b43 100644 --- a/extensions/typescript-language-features/src/tsServer/api.ts +++ b/extensions/typescript-language-features/src/tsServer/api.ts @@ -38,6 +38,7 @@ export class API { 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 87c715982c2b3..32707f1c0490a 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -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()) { 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 4f02ed29427e0..f1b0cca26a480 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 900d66f37abba..747e7c22e3724 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts @@ -19,70 +19,5 @@ declare module '../../../../node_modules/typescript/lib/typescript' { interface Response { readonly _serverType?: ServerType; } - - //#region MapCode - 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: FileCodeEdits[] - } - //#endregion - - //#region Paste - export interface GetPasteEditsRequest extends Request { - command: 'getPasteEdits'; - arguments: GetPasteEditsRequestArgs; - } - - export interface GetPasteEditsRequestArgs extends FileRequestArgs { - /** The text that gets pasted in a file. */ - pastedText: string[]; - /** Locations of where the `pastedText` gets added in a file. If the length of the `pastedText` and `pastedLocations` are not the same, - * then the `pastedText` is combined into one and added at all the `pastedLocations`. - */ - pasteLocations: TextSpan[]; - /** The source location of each `pastedText`. If present, the length of `spans` must be equal to the length of `pastedText`. */ - copiedFrom?: { - file: string; - spans: TextSpan[]; - }; - } - - export interface GetPasteEditsResponse extends Response { - body: PasteEditsAction; - } - export interface PasteEditsAction { - edits: FileCodeEdits[]; - fixId?: {}; - } - //#endregion } } - - diff --git a/extensions/typescript-language-features/src/typeConverters.ts b/extensions/typescript-language-features/src/typeConverters.ts index 58babe2bda39d..067a1ff3c0a90 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 da651e71044f3..c44bfc3ac3fd2 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/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 24742f99219fb..2435f7725a907 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/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 868f7cfd4e756..61828a784fbc8 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -7,13 +7,14 @@ "enabledApiProposals": [ "activeComment", "authSession", - "defaultChatParticipant", "chatParticipantPrivate", + "chatProvider", "chatVariableResolver", - "contribViewsRemote", "contribStatusBarItems", + "contribViewsRemote", "createFileSystemWatcher", "customEditorMove", + "defaultChatParticipant", "diffCommand", "documentFiltersExclusive", "documentPaste", @@ -27,6 +28,8 @@ "findTextInFiles", "fsChunks", "interactive", + "languageStatusText", + "lmTools", "mappedEditsProvider", "notebookCellExecutionState", "notebookDeprecated", @@ -35,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": [], @@ -63,6 +65,11 @@ }, "icon": "media/icon.png", "contributes": { + "languageModels": [ + { + "vendor": "test-lm-vendor" + } + ], "chatParticipants": [ { "id": "api-test.participant", @@ -105,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" } } }, @@ -242,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 0000000000000..178119a1197c7 --- /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 0000000000000..ccda85b442a1d --- /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 484fa0c5ac54b..9999a40aa144d 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 dac84162b3c8d..cc1450808afac 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 e1aa82e9ca36e..c8c2d57d90360 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 2066b677e2cca..e5267d6249425 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,64 @@ } }, { - "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", + "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": "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", + "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", @@ -210,8 +266,8 @@ } }, { - "c": " note is the introduction of the API concept, missing previously from Edge", - "t": "source.yaml string.quoted.single.yaml", + "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", @@ -224,8 +280,22 @@ } }, { - "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", @@ -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 856f013541b08..3381f6448d068 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 407cc7c7a1a6d..0908e19e3eae0 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", + "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/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index 8e12e622e05ac..2fab3ec306a27 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -164,8 +164,8 @@ export function activate(context: vscode.ExtensionContext) { const serverCommandPath = path.join(vscodePath, 'scripts', serverCommand); outputChannel.appendLine(`Launching server: "${serverCommandPath}" ${commandArgs.join(' ')}`); - - extHostProcess = cp.spawn(serverCommandPath, commandArgs, { env, cwd: vscodePath }); + const shell = (process.platform === 'win32'); + extHostProcess = cp.spawn(serverCommandPath, commandArgs, { env, cwd: vscodePath, shell }); } else { const extensionToInstall = process.env['TESTRESOLVER_INSTALL_BUILTIN_EXTENSION']; if (extensionToInstall) { @@ -182,8 +182,8 @@ export function activate(context: vscode.ExtensionContext) { outputChannel.appendLine(`Using server build at ${serverLocation}`); outputChannel.appendLine(`Server arguments ${commandArgs.join(' ')}`); - - extHostProcess = cp.spawn(path.join(serverLocation, 'bin', serverCommand), commandArgs, { env, cwd: serverLocation }); + const shell = (process.platform === 'win32'); + extHostProcess = cp.spawn(path.join(serverLocation, 'bin', serverCommand), commandArgs, { env, cwd: serverLocation, shell }); } extHostProcess.stdout!.on('data', (data: Buffer) => processOutput(data.toString())); extHostProcess.stderr!.on('data', (data: Buffer) => processOutput(data.toString())); diff --git a/extensions/yaml/build/update-grammar.js b/extensions/yaml/build/update-grammar.js new file mode 100644 index 0000000000000..8684bc3e5d047 --- /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 e6c3ca158b586..c75a4e52f89ca 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": "60e2e6e24c63d5a703cb04577678a2e416edd956" } }, "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.1" } ], "version": 1 diff --git a/extensions/yaml/package.json b/extensions/yaml/package.json index 5223f71c52b7a..d19c507bdfe47 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 0000000000000..6ca3d9bf1b89b --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.0.tmLanguage.json @@ -0,0 +1,1149 @@ +{ + "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/60e2e6e24c63d5a703cb04577678a2e416edd956", + "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": "source.yaml.1.1#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "begin": "\\G", + "while": "\\G", + "name": "meta.stream.yaml", + "patterns": [ + { + "include": "source.yaml.1.1#byte-order-mark" + }, + { + "include": "#directives" + }, + { + "include": "#document" + }, + { + "include": "source.yaml.1.1#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": "source.yaml.1.1#directive-invalid" + }, + { + "include": "#directives" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "begin": "\\G(?=---[\\x{85 2028 2029}\r\n\t ])", + "while": "\\G(?!%)", + "patterns": [ + { + "include": "#document" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "source.yaml.1.1#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": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "match": "\\G\\.{3}(?=[\\x{85 2028 2029}\r\n\t ])", + "name": "invalid.illegal.entity.other.document.end.yaml" + }, + { + "include": "source.yaml.1.1#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": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "source.yaml.1.1#byte-order-mark" + }, + { + "include": "source.yaml.1.1#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": "source.yaml.1.1#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "source.yaml.1.1#alias" + }, + { + "begin": "(?=\"|')", + "while": "\\G", + "patterns": [ + { + "begin": "(?!\\G)", + "while": "\\G", + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "source.yaml.1.1#single" + } + ] + }, + { + "begin": "(?={)", + "end": "$", + "patterns": [ + { + "include": "#flow-mapping" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "begin": "(?=\\[)", + "end": "$", + "patterns": [ + { + "include": "#flow-sequence" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "source.yaml.1.1#block-plain-out" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + "block-mapping": { + "//": "The check for plain keys is expensive", + "begin": "(?=((?<=[-?:]) )?+)(?[!&*][^\\x{85 2028 2029}\r\n\t ]*+[\t ]++)*+)(?=(?>(?#Double Quote)\"(?>[^\\\\\"]++|\\\\.)*+\"|(?#Single Quote)'(?>[^']++|'')*+'|(?#Flow-Map){(?>[^\\x{85 2028 2029}}]++|}[ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++}|(?#Flow-Seq)\\[(?>[^\\x{85 2028 2029}\\]]++|][ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++]|(?#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": "source.yaml.1.1#anchor-property" + }, + { + "include": "source.yaml.1.1#alias" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + }, + "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": "source.yaml.1.1#anchor-property" + }, + { + "include": "source.yaml.1.1#alias" + }, + { + "include": "source.yaml.1.1#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": "source.yaml#block-map-key-single" + }, + { + "include": "source.yaml.1.1#block-map-key-plain" + }, + { + "include": "#block-map-key-explicit" + }, + { + "include": "#block-map-value" + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + }, + { + "include": "source.yaml.1.1#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-key-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}]])((?>\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": "source.yaml#key-single" + }, + { + "include": "source.yaml.1.1#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-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}]]|-[^\\x{85 2028 2029}\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 ]*+)(?>(\\|)|(>))(?[+-])?+((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": "source.yaml#non-printable" + } + ] + }, + { + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "source.yaml.1.1#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": "source.yaml#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": "source.yaml#non-printable" + } + ] + } + ] + }, + { + "comment": "https://yaml.org/spec/1.2.2/#rule-l-chomped-empty", + "begin": "(?!\\G)(?=[\t ]*+#)", + "while": "\\G", + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + ] + }, + { + "comment": "Header Comment", + "begin": "\\G", + "end": "$", + "patterns": [ + { + "include": "source.yaml.1.1#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": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "#flow-mapping" + }, + { + "include": "#flow-sequence" + } + ] + }, + { + "include": "source.yaml.1.1#anchor-property" + }, + { + "include": "#tag-property" + }, + { + "include": "source.yaml.1.1#alias" + }, + { + "begin": "(?=\"|')", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "begin": "(?!\\G)", + "end": "(?=[:,\\]}])", + "patterns": [ + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + }, + { + "include": "#double" + }, + { + "include": "source.yaml.1.1#single" + } + ] + }, + { + "include": "source.yaml.1.1#flow-plain-in" + }, + { + "include": "source.yaml.1.1#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": "source.yaml.1.1#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(?=[\\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": "source.yaml.1.1#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": "\\?(?=[\\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-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": "(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\p{Cntrl}\\p{Surrogate}\\x{FFFE FFFF}]|[?:-](?![\\x{85 2028 2029}\r\n\t ,\\[\\]{}])))", + "end": "(?=[,\\[\\]{}])", + "name": "meta.flow.map.implicit.yaml", + "patterns": [ + { + "include": "source.yaml.1.1#flow-key-plain-in" + }, + { + "match": ":(?=\\[|{)", + "name": "invalid.illegal.separator.map.yaml" + }, + { + "include": "#flow-map-value-yaml" + }, + { + "include": "source.yaml.1.1#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": "source.yaml#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "source.yaml.1.1#presentation-detail" + } + ] + } + ] + }, + "flow-sequence-map-key": { + "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-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 ,\\[{]|^)(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\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": "source.yaml#key-single" + }, + { + "include": "#flow-map-value-json" + }, + { + "include": "source.yaml.1.1#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" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (FLOW-OUT)", + "begin": "\"", + "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" + } + ] + }, + "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-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": "source.yaml#non-printable" + } + ] + } + } +} \ 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 0000000000000..3b7974a1a5dfd --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.1.tmLanguage.json @@ -0,0 +1,1554 @@ +{ + "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/60e2e6e24c63d5a703cb04577678a2e416edd956", + "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": "source.yaml#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": "source.yaml.1.2#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": "(?={)", + "end": "$", + "patterns": [ + { + "include": "#flow-mapping" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "(?=\\[)", + "end": "$", + "patterns": [ + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "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)'(?>[^']++|'')*+'|(?#Flow-Map){(?>[^\\x{85 2028 2029}}]++|}[ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++}|(?#Flow-Seq)\\[(?>[^\\x{85 2028 2029}\\]]++|][ \t]*+(?!:[\\x{85 2028 2029}\r\n\t ]))++]|(?#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" + } + ] + } + }, + "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": "source.yaml#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)(?!-[\\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-key-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}]])((?>\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": "source.yaml#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-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": "source.yaml#non-printable" + } + ] + }, + "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}]]|-[^\\x{85 2028 2029}\r\n\t ])", + "beginCaptures": { + "0": { + "name": "punctuation.separator.map.value.yaml" + } + }, + "name": "meta.map.value.yaml", + "patterns": [ + { + "include": "#block-node" + } + ] + }, + "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": "source.yaml#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-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(?=[\\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-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": "\\?(?=[\\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-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": "(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\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": "source.yaml#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": "\\?(?=[\\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-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 ,\\[{]|^)(?=(?>[^-?:,\\[\\]{}#&*!|>'\"%@` \\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": "source.yaml#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": "source.yaml#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": "source.yaml#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": "source.yaml#non-printable" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (FLOW-OUT)", + "begin": "\"", + "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" + } + ] + }, + "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": "source.yaml#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": "source.yaml#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": "source.yaml#non-printable" + }, + { + "include": "#comment" + }, + { + "include": "#unknown" + } + ] + }, + "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": "source.yaml#non-printable" + } + ] + }, + "unknown": { + "match": ".[[^\\x{85 2028 2029}#\"':,\\[\\]{}]&&!-~\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", + "name": "invalid.illegal.unrecognized.yaml" + } + } +} \ 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 0000000000000..711f5c8e4222f --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.2.tmLanguage.json @@ -0,0 +1,1639 @@ +{ + "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/60e2e6e24c63d5a703cb04577678a2e416edd956", + "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": "source.yaml#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": "(?={)", + "end": "$", + "patterns": [ + { + "include": "#flow-mapping" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "begin": "(?=\\[)", + "end": "$", + "patterns": [ + { + "include": "#flow-sequence" + }, + { + "include": "#presentation-detail" + } + ] + }, + { + "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)'(?>[^']++|'')*+'|(?#Flow-Map){(?>[^}]++|}[ \t]*+(?!:[\r\n\t ]))++}|(?#Flow-Seq)\\[(?>[^]]++|][ \t]*+(?!:[\r\n\t ]))++]|(?#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" + } + ] + } + }, + "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": "source.yaml#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": "source.yaml#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-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": "source.yaml#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": "source.yaml#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": "source.yaml#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": "source.yaml#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": "source.yaml#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": "source.yaml#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": "source.yaml#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": "source.yaml#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": "source.yaml#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": "source.yaml#non-printable" + } + ] + }, + "key-double": { + "comment": "https://yaml.org/spec/1.2.2/#double-quoted-style (FLOW-OUT)", + "begin": "\"", + "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" + } + ] + }, + "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": "source.yaml#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": "source.yaml#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": "source.yaml#non-printable" + }, + { + "include": "#comment" + }, + { + "include": "#unknown" + } + ] + }, + "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": "source.yaml#non-printable" + } + ] + }, + "unknown": { + "match": ".[[^\"':,\\[\\]{}]&&!-~\\x{85}\\x{A0}-\\x{D7FF}\\x{E000}-\\x{FFFD}\\x{010000}-\\x{10FFFF}]*+", + "name": "invalid.illegal.unrecognized.yaml" + } + } +} \ 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 0000000000000..8df69f61c8c43 --- /dev/null +++ b/extensions/yaml/syntaxes/yaml-1.3.tmLanguage.json @@ -0,0 +1,60 @@ +{ + "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/274009903e20ac6dc37ba5763fb853744e28c9b2", + "name": "YAML 1.3", + "scopeName": "source.yaml.1.3", + "comment": "https://spec.yaml.io/main/spec/1.3.0/", + "patterns": [ + { + "include": "source.yaml" + } + ], + "repository": { + "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": "source.yaml.1.2#directive-invalid" + }, + { + "include": "source.yaml.1.2#directives" + }, + { + "include": "source.yaml.1.2#presentation-detail" + } + ] + }, + { + "include": "source.yaml.1.2#document" + } + ] + } + } +} \ No newline at end of file diff --git a/extensions/yaml/syntaxes/yaml.tmLanguage.json b/extensions/yaml/syntaxes/yaml.tmLanguage.json index 447df713901d0..8be76d7a3c595 100644 --- a/extensions/yaml/syntaxes/yaml.tmLanguage.json +++ b/extensions/yaml/syntaxes/yaml.tmLanguage.json @@ -1,621 +1,95 @@ { "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/274009903e20ac6dc37ba5763fb853744e28c9b2", + "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" - } - ] + "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 YAML versions 1.3, 1.2, 1.1 and 1.0" }, - "flow-scalar-double-quoted": { - "begin": "\"", + "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" } }, - "end": "\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.yaml" } }, - "name": "string.quoted.double.yaml", + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.yaml", "patterns": [ { - "match": "\\\\([0abtnvfre \"/\\\\N_Lp]|x\\d\\d|u\\d{4}|U\\d{8})", - "name": "constant.character.escape.yaml" + "match": ".[\t ]*+$", + "name": "invalid.illegal.multiline-key.yaml" }, { - "match": "\\\\\\n", - "name": "constant.character.escape.double-quoted.newline.yaml" - } - ] - }, - "flow-scalar-plain-in": { - "patterns": [ - { - "include": "#flow-scalar-plain-in-implicit-type" + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.yaml" }, { - "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 " + "match": "''", + "name": "constant.character.escape.single-quote.yaml" } ] }, - "flow-scalar-single-quoted": { + "key-single": { + "comment": "https://yaml.org/spec/1.2.2/#rule-c-single-quoted (FLOW-OUT)", "begin": "'", + "end": "'(?!')", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.yaml" } }, - "end": "'(?!')", "endCaptures": { "0": { "name": "punctuation.definition.string.end.yaml" } }, - "name": "string.quoted.single.yaml", + "name": "meta.map.key.yaml string.quoted.single.yaml entity.name.tag.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" + "match": "[^\t -\\x{10FFFF}]++", + "name": "invalid.illegal.character.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" - } - ] + "match": "''", + "name": "constant.character.escape.single-quote.yaml" + } + ] + }, + "non-printable": { + "//": { + "85": "Â…", + "2028": "", + "2029": "", + "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" } } } \ No newline at end of file diff --git a/extensions/yarn.lock b/extensions/yarn.lock index fa4595ffa747f..2ac598390f82c 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,120 +2,125 @@ # yarn lockfile v1 -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== - -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== - -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== - -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== - -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== - -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== - -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== - -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== - -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== - -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== - -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== - -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== - -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== - -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== - -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== - -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== - -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== - -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== - -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== - -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== - -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== - -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== - -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== +"@esbuild/aix-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" + integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== + +"@esbuild/android-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" + integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== + +"@esbuild/android-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" + integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== + +"@esbuild/android-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" + integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== + +"@esbuild/darwin-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" + integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== + +"@esbuild/darwin-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" + integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== + +"@esbuild/freebsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" + integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== + +"@esbuild/freebsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" + integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== + +"@esbuild/linux-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" + integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== + +"@esbuild/linux-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" + integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== + +"@esbuild/linux-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" + integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== + +"@esbuild/linux-loong64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" + integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== + +"@esbuild/linux-mips64el@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" + integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== + +"@esbuild/linux-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" + integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== + +"@esbuild/linux-riscv64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" + integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== + +"@esbuild/linux-s390x@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" + integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== + +"@esbuild/linux-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" + integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== + +"@esbuild/netbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" + integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== + +"@esbuild/openbsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" + integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== + +"@esbuild/openbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" + integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== + +"@esbuild/sunos-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" + integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== + +"@esbuild/win32-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" + integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== + +"@esbuild/win32-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" + integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== + +"@esbuild/win32-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" + integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== "@parcel/watcher@2.1.0": version "2.1.0" @@ -128,11 +133,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" @@ -146,44 +151,45 @@ cson-parser@^4.0.9: dependencies: coffeescript "1.12.7" -esbuild@0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== +esbuild@0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599" + integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" + "@esbuild/aix-ppc64" "0.23.0" + "@esbuild/android-arm" "0.23.0" + "@esbuild/android-arm64" "0.23.0" + "@esbuild/android-x64" "0.23.0" + "@esbuild/darwin-arm64" "0.23.0" + "@esbuild/darwin-x64" "0.23.0" + "@esbuild/freebsd-arm64" "0.23.0" + "@esbuild/freebsd-x64" "0.23.0" + "@esbuild/linux-arm" "0.23.0" + "@esbuild/linux-arm64" "0.23.0" + "@esbuild/linux-ia32" "0.23.0" + "@esbuild/linux-loong64" "0.23.0" + "@esbuild/linux-mips64el" "0.23.0" + "@esbuild/linux-ppc64" "0.23.0" + "@esbuild/linux-riscv64" "0.23.0" + "@esbuild/linux-s390x" "0.23.0" + "@esbuild/linux-x64" "0.23.0" + "@esbuild/netbsd-x64" "0.23.0" + "@esbuild/openbsd-arm64" "0.23.0" + "@esbuild/openbsd-x64" "0.23.0" + "@esbuild/sunos-x64" "0.23.0" + "@esbuild/win32-arm64" "0.23.0" + "@esbuild/win32-ia32" "0.23.0" + "@esbuild/win32-x64" "0.23.0" fast-plist@0.1.2: version "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 +240,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 1965949114daf..8ed3111280b41 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.91.0", - "distro": "4729d9dae7d87c18fbe0614f875a60672e2b8603", + "version": "1.92.0", + "distro": "7616d7872f74568746eefa34266a5ab0a969604b", "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", - "kerberos": "^2.0.1", + "jschardet": "3.1.3", + "kerberos": "2.1.1-alpha.0", "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", @@ -136,8 +136,8 @@ "@vscode/l10n-dev": "0.0.35", "@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-electron": "^2.4.0", + "@vscode/test-web": "^0.0.56", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.14", "ansi-colors": "^3.2.3", @@ -149,7 +149,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "29.4.0", + "electron": "30.1.2", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", @@ -208,7 +208,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 eb8a1b26a1eb4..9a6ad239b7dd0 100644 --- a/product.json +++ b/product.json @@ -50,8 +50,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.90.0", - "sha256": "1317dd7d1ac50641c1534a3e957ecbc94349f4fbd897acb916da11eea3208a66", + "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 4c99388e88979..8d07643c18e5b 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,5 +1,5 @@ disturl "https://nodejs.org/dist" -target "20.11.1" -ms_build_id "275039" +target "20.14.0" +ms_build_id "282653" runtime "node" build_from_source "true" diff --git a/remote/package.json b/remote/package.json index b84f5136fb171..c455896477449 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,25 +8,25 @@ "@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", - "kerberos": "^2.0.1", + "jschardet": "3.1.3", + "kerberos": "2.1.1-alpha.0", "minimist": "^1.2.6", "native-watchdog": "^1.4.1", "node-pty": "1.1.0-beta11", diff --git a/remote/web/package.json b/remote/web/package.json index 76375094a7dae..f6b57db5ea0e7 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 81215235ac655..a28d00372f719 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-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-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-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-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/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== - -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/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-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-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-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-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/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.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 4241bf03b14ac..7ca95a9262932 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -66,10 +66,10 @@ 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" @@ -122,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-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-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-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-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/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/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-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-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-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-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-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/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/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" @@ -164,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" @@ -186,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" @@ -263,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" @@ -289,7 +303,7 @@ github-from-package@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: +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== @@ -325,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" @@ -347,10 +364,20 @@ 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" @@ -361,14 +388,14 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -kerberos@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.0.1.tgz#663b0b46883b4da84495f60f2e9e399a43a33ef5" - integrity sha512-O/jIgbdGK566eUhFwIcgalbqirYU/r76MW7/UFw06Fd9x5bSwgyZWL/Vm26aAmezQww/G9KYkmmJBkEkPk5HLw== +kerberos@2.1.1-alpha.0: + version "2.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.1.1-alpha.0.tgz#c6d377b43c8206340fd184167754f2c81dad5ab1" + integrity sha512-II8N/ky/Vpd8y7LTxwCuAYoQ8XeV3HYxuK7IDmyoFacIhDljx4sdt/+sOwqgXEweQyCHlbZSKSaK82upqNM4Hw== dependencies: bindings "^1.5.0" - node-addon-api "^4.3.0" - prebuild-install "7.1.1" + node-addon-api "^6.1.0" + prebuild-install "^7.1.2" lru-cache@^6.0.0: version "6.0.0" @@ -442,10 +469,10 @@ node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== -node-addon-api@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" - integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-addon-api@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== node-gyp-build@4.8.1, node-gyp-build@^4.3.0: version "4.8.1" @@ -476,10 +503,10 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -prebuild-install@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== +prebuild-install@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" + integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== dependencies: detect-libc "^2.0.0" expand-template "^2.0.3" @@ -558,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" diff --git a/resources/linux/snap/snapcraft.yaml b/resources/linux/snap/snapcraft.yaml index b7b93f4c59c5f..1d7412bdc71ff 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/helpers/check-requirements-linux.sh b/resources/server/bin/helpers/check-requirements-linux.sh index 079557869e33a..31a618fbd855e 100644 --- a/resources/server/bin/helpers/check-requirements-linux.sh +++ b/resources/server/bin/helpers/check-requirements-linux.sh @@ -27,6 +27,7 @@ fi ARCH=$(uname -m) found_required_glibc=0 found_required_glibcxx=0 +MIN_GLIBCXX_VERSION="3.4.25" # Extract the ID value from /etc/os-release if [ -f /etc/os-release ]; then @@ -40,7 +41,10 @@ fi # Based on https://github.com/bminor/glibc/blob/520b1df08de68a3de328b65a25b86300a7ddf512/elf/cache.c#L162-L245 case $ARCH in x86_64) LDCONFIG_ARCH="x86-64";; - armv7l | armv8l) LDCONFIG_ARCH="hard-float";; + armv7l | armv8l) + MIN_GLIBCXX_VERSION="3.4.26" + LDCONFIG_ARCH="hard-float" + ;; arm64 | aarch64) BITNESS=$(getconf LONG_BIT) if [ "$BITNESS" = "32" ]; then @@ -81,7 +85,7 @@ if [ "$OS_ID" != "alpine" ]; then libstdcpp_real_path=$(readlink -f "$libstdcpp_path_line") libstdcpp_version=$(grep -ao 'GLIBCXX_[0-9]*\.[0-9]*\.[0-9]*' "$libstdcpp_real_path" | sort -V | tail -1) libstdcpp_version_number=$(echo "$libstdcpp_version" | sed 's/GLIBCXX_//') - if [ "$(printf '%s\n' "3.4.24" "$libstdcpp_version_number" | sort -V | head -n1)" = "3.4.24" ]; then + if [ "$(printf '%s\n' "$MIN_GLIBCXX_VERSION" "$libstdcpp_version_number" | sort -V | head -n1)" = "$MIN_GLIBCXX_VERSION" ]; then found_required_glibcxx=1 break fi @@ -92,7 +96,7 @@ else found_required_glibcxx=1 fi if [ "$found_required_glibcxx" = "0" ]; then - echo "Warning: Missing GLIBCXX >= 3.4.25! from $libstdcpp_real_path" + echo "Warning: Missing GLIBCXX >= $MIN_GLIBCXX_VERSION! from $libstdcpp_real_path" fi if [ "$OS_ID" = "alpine" ]; then diff --git a/scripts/code-server.js b/scripts/code-server.js index c043bf2671b64..56945e76ca7c5 100644 --- a/scripts/code-server.js +++ b/scripts/code-server.js @@ -69,4 +69,3 @@ function startServer(programArgs) { } main(); - diff --git a/scripts/code.bat b/scripts/code.bat index 008c54fcbde25..7f48b7535593c 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 24929fdf3513e..c29b632cbcbc0 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 1c2074ee19134..474f83def861f 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]; - - ___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 relativePath = data.changedPath.replace(/\\/g, '/').split('/out/')[1]; + changes.push({ config: {}, path: data.changedPath, relativePath, newContent: data.newContent }); + } + + 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 851b296af62c5..8ede619160b73 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 cc47b050fb54d..f8a0c00be9513 100644 --- a/src/bootstrap-amd.js +++ b/src/bootstrap-amd.js @@ -6,6 +6,29 @@ //@ts-check 'use strict'; +/** + * @import { INLSConfiguration } from './vs/nls' + * @import { IProductConfiguration } from './vs/base/common/product' + */ + +// ESM-comment-begin +const isESM = false; +// ESM-comment-end +// ESM-uncomment-begin +// import * as path from 'path'; +// import * as fs from 'fs'; +// import { fileURLToPath } from 'url'; +// import { createRequire } from 'node:module'; +// import { product, pkg } from './bootstrap-meta.js'; +// import * as bootstrap from './bootstrap.js'; +// import * as performance from './vs/base/common/performance.js'; +// +// const require = createRequire(import.meta.url); +// const isESM = true; +// const module = { exports: {} }; +// const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// ESM-uncomment-end + // 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 +38,13 @@ 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 */ +// ESM-comment-begin +globalThis._VSCODE_PRODUCT_JSON = require('./bootstrap-meta').product; +// ESM-comment-end +// ESM-uncomment-begin +// globalThis._VSCODE_PRODUCT_JSON = { ...product }; +// ESM-uncomment-end if (process.env['VSCODE_DEV']) { // Patch product overrides when running out of sources try { @@ -25,63 +53,182 @@ 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'); +// ESM-comment-begin +globalThis._VSCODE_PACKAGE_JSON = require('./bootstrap-meta').pkg; +// ESM-comment-end +// ESM-uncomment-begin +// globalThis._VSCODE_PACKAGE_JSON = { ...pkg }; +// ESM-uncomment-end + +// VSCODE_GLOBALS: file root of all resources +globalThis._VSCODE_FILE_ROOT = __dirname; -// @ts-ignore -const loader = require('./vs/loader'); +// ESM-comment-begin const bootstrap = require('./bootstrap'); -const performance = require('./vs/base/common/performance'); - -// Bootstrap: NLS -const nlsConfig = bootstrap.setupNLS(); - -// Bootstrap: Loader -loader.config({ - baseUrl: bootstrap.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }), - catchError: true, - nodeRequire, - 'vs/nls': nlsConfig, - amdModulesPattern: /^vs\//, - recordStats: true -}); - -// Running in Electron -if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { - loader.define('fs', ['original-fs'], function (/** @type {import('fs')} */originalFS) { - return originalFS; // replace the patched electron fs with the original node fs for all AMD code - }); -} +const performance = require(`./vs/base/common/performance`); +const fs = require('fs'); +// ESM-comment-end -// 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; } /** - * @param {string=} entrypoint - * @param {(value: any) => void=} onLoad - * @param {(err: Error) => void=} onError + * @returns {Promise} */ -exports.load = function (entrypoint, onLoad, onError) { - if (!entrypoint) { - return; +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}`); + } } - // code cache config - if (process.env['VSCODE_CODE_CACHE_PATH']) { - loader.config({ - nodeCachedData: { - path: process.env['VSCODE_CODE_CACHE_PATH'], - seed: entrypoint + 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 + +//#region Loader Config + +if (isESM) { + + /** + * @param {string=} entrypoint + * @param {(value: any) => void} [onLoad] + * @param {(err: Error) => void} [onError] + */ + module.exports.load = function (entrypoint, onLoad, onError) { + if (!entrypoint) { + return; + } + + entrypoint = `./${entrypoint}.js`; + + onLoad = onLoad || function () { }; + onError = onError || function (err) { console.error(err); }; + + setupNLS().then(() => { + performance.mark(`code/fork/willLoadCode`); + import(entrypoint).then(onLoad, onError); + }); + }; +} else { + + // @ts-ignore + const loader = require('./vs/loader'); + + loader.config({ + baseUrl: bootstrap.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }), + catchError: true, + nodeRequire, + amdModulesPattern: /^vs\//, + recordStats: true + }); + + // Running in Electron + if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) { + loader.define('fs', ['original-fs'], function (/** @type {import('fs')} */originalFS) { + return originalFS; // replace the patched electron fs with the original node fs for all AMD code }); } - onLoad = onLoad || function () { }; - onError = onError || function (err) { console.error(err); }; + /** + * @param {string=} entrypoint + * @param {(value: any) => void} [onLoad] + * @param {(err: Error) => void} [onError] + */ + module.exports.load = function (entrypoint, onLoad, onError) { + if (!entrypoint) { + return; + } + + // code cache config + if (process.env['VSCODE_CODE_CACHE_PATH']) { + loader.config({ + nodeCachedData: { + path: process.env['VSCODE_CODE_CACHE_PATH'], + seed: entrypoint + } + }); + } + + onLoad = onLoad || function () { }; + onError = onError || function (err) { console.error(err); }; + + setupNLS().then(() => { + performance.mark('code/fork/willLoadCode'); + loader([entrypoint], onLoad, onError); + }); + }; +} + +//#endregion - performance.mark('code/fork/willLoadCode'); - loader([entrypoint], onLoad, onError); -}; +// ESM-uncomment-begin +// export const load = module.exports.load; +// ESM-uncomment-end diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 9de1e6f0d1590..a95cbf53589d5 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -6,11 +6,20 @@ //@ts-check 'use strict'; +// ESM-comment-begin const performance = require('./vs/base/common/performance'); -performance.mark('code/fork/start'); - const bootstrap = require('./bootstrap'); const bootstrapNode = require('./bootstrap-node'); +const bootstrapAmd = require('./bootstrap-amd'); +// ESM-comment-end +// ESM-uncomment-begin +// import * as performance from './vs/base/common/performance.js'; +// import * as bootstrap from './bootstrap.js'; +// import * as bootstrapNode from './bootstrap-node.js'; +// import * as bootstrapAmd from './bootstrap-amd.js'; +// ESM-uncomment-end + +performance.mark('code/fork/start'); // Crash reporter configureCrashReporter(); @@ -41,7 +50,7 @@ if (process.env['VSCODE_PARENT_PID']) { } // Load AMD entry point -require('./bootstrap-amd').load(process.env['VSCODE_AMD_ENTRYPOINT']); +bootstrapAmd.load(process.env['VSCODE_AMD_ENTRYPOINT']); //#region Helpers diff --git a/src/bootstrap-meta.js b/src/bootstrap-meta.js new file mode 100644 index 0000000000000..5415bc5df6a68 --- /dev/null +++ b/src/bootstrap-meta.js @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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' + */ + +// ESM-uncomment-begin +// import { createRequire } from 'node:module'; +// +// const require = createRequire(import.meta.url); +// const module = { exports: {} }; +// ESM-uncomment-end + +/** @type Partial & { BUILD_INSERT_PRODUCT_CONFIGURATION?: string } */ +let productObj = { BUILD_INSERT_PRODUCT_CONFIGURATION: 'BUILD_INSERT_PRODUCT_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD +if (productObj['BUILD_INSERT_PRODUCT_CONFIGURATION']) { + // @ts-ignore + productObj = require('../product.json'); // Running out of sources +} + +/** @type object & { BUILD_INSERT_PACKAGE_CONFIGURATION?: string } */ +let pkgObj = { BUILD_INSERT_PACKAGE_CONFIGURATION: 'BUILD_INSERT_PACKAGE_CONFIGURATION' }; // DO NOT MODIFY, PATCHED DURING BUILD +if (pkgObj['BUILD_INSERT_PACKAGE_CONFIGURATION']) { + // @ts-ignore + pkgObj = require('../package.json'); // Running out of sources +} + +module.exports.product = productObj; +module.exports.pkg = pkgObj; + +// ESM-uncomment-begin +// export const product = module.exports.product; +// export const pkg = module.exports.pkg; +// ESM-uncomment-end diff --git a/src/bootstrap-node.js b/src/bootstrap-node.js index 914b829038013..4a829557f0d45 100644 --- a/src/bootstrap-node.js +++ b/src/bootstrap-node.js @@ -6,12 +6,28 @@ //@ts-check 'use strict'; +// ESM-comment-begin +const path = require('path'); +const fs = require('fs'); + +const isESM = false; +// ESM-comment-end +// ESM-uncomment-begin +// import * as path from 'path'; +// import * as fs from 'fs'; +// import { fileURLToPath } from 'url'; +// import { createRequire } from 'node:module'; +// +// const require = createRequire(import.meta.url); +// const isESM = true; +// const module = { exports: {} }; +// const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// ESM-uncomment-end + // Setup current working directory in all our node & electron processes // - Windows: call `process.chdir()` to always set application folder as cwd // - all OS: store the `process.cwd()` inside `VSCODE_CWD` for consistent lookups function setupCurrentWorkingDirectory() { - const path = require('path'); - try { // Store the `process.cwd()` inside `VSCODE_CWD` @@ -38,36 +54,41 @@ setupCurrentWorkingDirectory(); * * @param {string} injectPath */ -exports.injectNodeModuleLookupPath = function (injectPath) { +module.exports.injectNodeModuleLookupPath = function (injectPath) { if (!injectPath) { throw new Error('Missing injectPath'); } - const Module = require('module'); - const path = require('path'); - - const nodeModulesPath = path.join(__dirname, '../node_modules'); + const Module = require('node:module'); + if (isESM) { + // register a loader hook + // ESM-uncomment-begin + // Module.register('./server-loader.mjs', { parentURL: import.meta.url, data: injectPath }); + // ESM-uncomment-end + } else { + const nodeModulesPath = path.join(__dirname, '../node_modules'); - // @ts-ignore - const originalResolveLookupPaths = Module._resolveLookupPaths; + // @ts-ignore + const originalResolveLookupPaths = Module._resolveLookupPaths; - // @ts-ignore - Module._resolveLookupPaths = function (moduleName, parent) { - const paths = originalResolveLookupPaths(moduleName, parent); - if (Array.isArray(paths)) { - for (let i = 0, len = paths.length; i < len; i++) { - if (paths[i] === nodeModulesPath) { - paths.splice(i, 0, injectPath); - break; + // @ts-ignore + Module._resolveLookupPaths = function (moduleName, parent) { + const paths = originalResolveLookupPaths(moduleName, parent); + if (Array.isArray(paths)) { + for (let i = 0, len = paths.length; i < len; i++) { + if (paths[i] === nodeModulesPath) { + paths.splice(i, 0, injectPath); + break; + } } } - } - return paths; - }; + return paths; + }; + } }; -exports.removeGlobalNodeModuleLookupPaths = function () { +module.exports.removeGlobalNodeModuleLookupPaths = function () { const Module = require('module'); // @ts-ignore const globalPaths = Module.globalPaths; @@ -95,10 +116,7 @@ exports.removeGlobalNodeModuleLookupPaths = function () { * @param {Partial} product * @returns {{ portableDataPath: string; isPortable: boolean; }} */ -exports.configurePortable = function (product) { - const fs = require('fs'); - const path = require('path'); - +module.exports.configurePortable = function (product) { const appRoot = path.dirname(__dirname); /** @@ -158,3 +176,9 @@ exports.configurePortable = function (product) { isPortable }; }; + +// ESM-uncomment-begin +// export const injectNodeModuleLookupPath = module.exports.injectNodeModuleLookupPath; +// export const removeGlobalNodeModuleLookupPaths = module.exports.removeGlobalNodeModuleLookupPaths; +// export const configurePortable = module.exports.configurePortable; +// ESM-uncomment-end diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index fa3bc5eb839dc..59ddc3fdfbf38 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -8,10 +8,22 @@ //@ts-check 'use strict'; -/* eslint-disable no-restricted-globals */ +/** + * @import { ISandboxConfiguration } from './vs/base/parts/sandbox/common/sandboxTypes' + * @typedef {any} LoaderConfig + */ + +/* eslint-disable no-restricted-globals, */ + +// ESM-comment-begin +const isESM = false; +// ESM-comment-end +// ESM-uncomment-begin +// const isESM = true; +// ESM-uncomment-end // Simple module style to support node.js and browser environments -(function (globalThis, factory) { +(function (factory) { // Node.js if (typeof exports === 'object') { @@ -23,14 +35,12 @@ // @ts-ignore globalThis.MonacoBootstrapWindow = factory(); } -}(this, function () { +}(function () { const bootstrapLib = bootstrap(); const preloadGlobals = sandboxGlobals(); 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,84 +90,151 @@ 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} */ - const loaderConfig = { - baseUrl: `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32', scheme: 'vscode-file', fallbackAuthority: 'vscode-app' })}/out`, - 'vs/nls': nlsConfig, - preferScriptTags: true - }; + if (isESM) { - // use a trusted types policy when loading via script tags - loaderConfig.trustedTypesPolicy = window.trustedTypes?.createPolicy('amdLoader', { - createScriptURL(value) { - if (value.startsWith(window.location.origin)) { - return value; + // Signal before require() + if (typeof options?.beforeRequire === 'function') { + options.beforeRequire(configuration); + } + + const fileRoot = `${configuration.appRoot}/out`; + globalThis._VSCODE_FILE_ROOT = fileRoot; + + // DEV --------------------------------------------------------------------------------------- + // DEV: This is for development and enables loading CSS via import-statements via import-maps. + // DEV: For each CSS modules that we have we defined an entry in the import map that maps to + // DEV: a blob URL that loads the CSS via a dynamic @import-rule. + // DEV --------------------------------------------------------------------------------------- + if (configuration.cssModules) { + performance.mark('code/willAddCssLoader'); + + const style = document.createElement('style'); + style.type = 'text/css'; + style.media = 'screen'; + style.id = 'vscode-css-loading'; + document.head.appendChild(style); + + globalThis._VSCODE_CSS_LOAD = function (url) { + style.textContent += `@import url(${url});\n`; + }; + + const baseUrl = new URL(`vscode-file://vscode-app${fileRoot}/`); + /** + * @type { { imports: Record }} + */ + const importMap = { imports: {} }; + for (const cssModule of configuration.cssModules) { + const cssUrl = new URL(cssModule, baseUrl).href; + const jsSrc = `globalThis._VSCODE_CSS_LOAD('${cssUrl}');\n`; + const blob = new Blob([jsSrc], { type: 'application/javascript' }); + importMap.imports[cssUrl] = URL.createObjectURL(blob); } - throw new Error(`Invalid script url: ${value}`); + + const ttp = window.trustedTypes?.createPolicy('vscode-bootstrapImportMap', { createScript(value) { return value; }, }); + const importMapSrc = JSON.stringify(importMap, undefined, 2); + const importMapScript = document.createElement('script'); + importMapScript.type = 'importmap'; + importMapScript.setAttribute('nonce', '0c6a828f1297'); + // @ts-ignore + importMapScript.textContent = ttp?.createScript(importMapSrc) ?? importMapSrc; + document.head.appendChild(importMapScript); + + performance.mark('code/didAddCssLoader'); } - }); - - // Teach the loader the location of the node modules we use in renderers - // This will enable to load these modules via - + + + + diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 46193cdbe9779..ca405dd725d77 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 8548fa7ce4d87..deb0d1eab5624 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 8234b734d06ca..98e67e7c25c06 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -4,8 +4,13 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check +'use strict'; + (function () { - 'use strict'; + + /** + * @import { ISandboxConfiguration } from '../../../base/parts/sandbox/common/sandboxTypes' + */ const bootstrapWindow = bootstrapWindowLib(); @@ -21,12 +26,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 35e8368d3c9f6..5ad97839d233d 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -6,21 +6,27 @@ /// //@ts-check +'use strict'; + (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 +51,7 @@ showSplash(windowConfig); }, beforeLoaderConfig: function (loaderConfig) { + // @ts-ignore loaderConfig.recordStats = true; }, beforeRequire: function (windowConfig) { @@ -74,14 +81,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 +132,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 +167,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 +179,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/cli.ts b/src/vs/code/node/cli.ts index 211d1dbf3e535..fb6ad785025d3 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -39,10 +39,6 @@ function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { || !!argv['telemetry']; } -interface IMainCli { - main: (argv: NativeParsedArgs) => Promise; -} - export async function main(argv: string[]): Promise { let args: NativeParsedArgs; @@ -112,7 +108,8 @@ export async function main(argv: string[]): Promise { // Extensions Management else if (shouldSpawnCliProcess(args)) { - const cli = await new Promise((resolve, reject) => require(['vs/code/node/cliProcessMain'], resolve, reject)); + + const cli = await import('vs/code/node/cliProcessMain'); await cli.main(args); return; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 2c1d7afc54c58..f940e372ab39a 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 77ae5d9786a3e..7ca147e384d0a 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 68d2cc1666151..0c7e517d4bc73 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 f8e915491fc46..69c81ac249896 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 90bafb662845e..e1d36f142bf3d 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 c8e5b7e50a47c..a5f824ca45065 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 ca0a4bb8a1f2c..e7f0743af8318 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 0000000000000..195aee2cf4fa0 --- /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 7bcfe7ecd6ac0..0000000000000 --- 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 b960f48191e1a..1fedb4d17c42c 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 0c554ca039ebc..20faf6cfa8ce2 100644 --- a/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -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 { @@ -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 ((isHTMLElement(e.target)) && 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 ((isHTMLElement(e.target)) && 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/updatableHoverWidget.ts b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts index 3b746de6f43c4..cf9b355831f8b 100644 --- a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isHTMLElement } from 'vs/base/browser/dom'; -import type { IHoverWidget, IUpdatableHoverContent, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; +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'; @@ -13,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; @@ -23,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); @@ -64,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)) { @@ -86,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 986e18ac6c03d..4d7539435cd35 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.ts b/src/vs/editor/browser/view.ts index 5558cf28cd4b6..cd04dd2dcc639 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -399,6 +399,14 @@ export class View extends ViewEventHandler { } super.dispose(); + + // See https://github.com/microsoft/vscode/pull/219297 + // Reduces the impact of detached dom node memory leaks + // E.g when a memory leak occurs in a child component + // of the editor (e.g. Minimap, GlyphMarginWidgets,...) + // the editor dom (linked to the child dom) should not be + // leaked as well. + dom.clearNodeRecursively(this.domNode.domNode); } private _scheduleRender(): void { diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 64fb2185eedae..2861fd8f82a7e 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 bbbb0dd9d7314..971d8ae40114d 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -295,9 +295,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(); } } @@ -310,9 +308,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(); } } @@ -481,9 +477,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); } @@ -502,9 +496,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 a11435b8e7373..bdf8eb77d210b 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 48c79a783ead7..164b2299b75c4 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/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 37914a70335d2..a99dac77cde29 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 07753688f7ed9..f8ed64fba6c7c 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 db99842d621eb..55b5d4a1e5489 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 23a75bac47d12..6a5283f24917d 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 f5bbdb1b43faa..b53bb2660e3de 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 b1520c8821f0c..67f8505cc28d9 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 c3c798529caf9..9c23977877319 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 { diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts index e7c76c727cf6f..010fba5ec96ed 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)); diff --git a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts index 7d0d3a992c15b..afb29693f817e 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 8141cd9452cf0..017d8268f6e98 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 36bd4d465ff23..80b553bcb30a0 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/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 76c37c4185dbe..1e6d5eee42878 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(); }); } @@ -298,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 1c3341a73ef14..a301fc6124bf3 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 d58781aabfeb1..297e5e864652c 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 a044eb438edb0..b58d7303e667d 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 496b002489b50..8275c4f734582 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 c29fc74bddd4e..96cccc0afb8ce 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 41354c4711866..643944437650a 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. @@ -3347,6 +3354,25 @@ class EditorPixelRatio extends ComputedEditorOption { + 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'; @@ -4145,8 +4171,6 @@ export interface IInlineEditOptions { * Does not clear active inline suggestions when the editor loses focus. */ keepOnBlur?: boolean; - - backgroundColoring?: boolean; } /** @@ -4161,7 +4185,6 @@ class InlineEditorEdit 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 4df16ec8db26a..f84b5135d13d1 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 0000000000000..df17d2f3918cd --- /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 ffa80cbb63ca7..b4c65156f8511 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 b16172cc89a9f..a43538215b232 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 50fbf66deba9c..2de4635030d19 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 8f7183211d7d5..6a46557a17719 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 2c3ee2fe09426..5573f68452621 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(); @@ -167,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; @@ -208,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) => @@ -229,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 { @@ -312,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 9edc63d335f92..7761599484b07 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[] = []; - - constructor(public readonly lines: string[], lineRange: OffsetRange, public readonly considerWhitespaceChanges: boolean) { - // This slice has to have lineRange.length many \n! (otherwise diffing against an empty slice will be problematic) - // (Unless it covers the entire document, in that case the other slice also has to cover the entire document ands it's okay) - - // If the slice covers the end, but does not start at the beginning, we include just the \n of the previous line. - let trimFirstLineFully = false; - if (lineRange.start > 0 && lineRange.endExclusive >= lines.length) { - lineRange = new OffsetRange(lineRange.start - 1, lineRange.endExclusive); - trimFirstLineFully = true; - } + private readonly firstElementOffsetByLineIdx: number[] = []; + private readonly lineStartOffsets: number[] = []; + private readonly trimmedWsLengthsByLineIdx: number[] = []; + + 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); - 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 f15b2a053100c..da9c3a49109f8 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 53c14ac57b9ed..47679f308f475 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 68fbf632fb4cf..3159cdc6ae5da 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 680d3a7d5f9f0..5c643b4fa604f 100644 --- a/src/vs/editor/common/languages/autoIndent.ts +++ b/src/vs/editor/common/languages/autoIndent.ts @@ -12,6 +12,7 @@ import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions' 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: { @@ -357,13 +358,14 @@ 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; } @@ -404,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; } diff --git a/src/vs/editor/common/languages/supports/richEditBrackets.ts b/src/vs/editor/common/languages/supports/richEditBrackets.ts index 7733719f04975..abb308504665a 100644 --- a/src/vs/editor/common/languages/supports/richEditBrackets.ts +++ b/src/vs/editor/common/languages/supports/richEditBrackets.ts @@ -7,7 +7,6 @@ import * as strings from 'vs/base/common/strings'; import * as stringBuilder from 'vs/editor/common/core/stringBuilder'; import { Range } from 'vs/editor/common/core/range'; import { CharacterPair } from 'vs/editor/common/languages/languageConfiguration'; -import { RegExpOptions } from 'vs/base/common/strings'; interface InternalBracket { open: string[]; @@ -409,7 +408,7 @@ function prepareBracketForRegExp(str: string): string { return (insertWordBoundaries ? `\\b${str}\\b` : str); } -export function createBracketOrRegExp(pieces: string[], options?: RegExpOptions): RegExp { +export function createBracketOrRegExp(pieces: string[], options?: strings.RegExpOptions): RegExp { const regexStr = `(${pieces.map(prepareBracketForRegExp).join(')|(')})`; return strings.createRegExp(regexStr, true, options); } diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index e21aa7d600c7a..134234bbfe59b 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 470a16f009c81..3d3fe2e3649e1 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 b75d0d75a70ef..24f90651f9502 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 12d7e0b098179..a369298c0c97b 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 626217e8bffd9..97b5a483fc350 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 804f63c6a2800..40c6c921afc34 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 3608f3042a856..52a1e2633e639 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 f248e0e23c2b9..1bb2e0d6ed15f 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 4c5c84ee0f6c7..7cd16daae8b8f 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/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index c5af99cca77bf..50366d5495827 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 ffd9e3240dd8f..b3530eaa88834 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 289fe8aa96a61..7a5f7e106e79a 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 65dc839da2cf5..612fd4d292305 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 ea05c8574b737..cbadb2348ef61 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 94518efd41557..45fc9aa9cb77d 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 c1783a39f1188..a98da2b0e0da3 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 641f1491d6f05..664a36b2dcad5 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 71d6df33c0b4f..5946fb24f5047 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 67fa3b8009c86..f7666b8c24692 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 e37a442f643b0..32956ec08da1d 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 7688defca5f24..b911fe5b0f5e7 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 4c8cf8652690f..424448611b18f 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 ff394a835bb9a..f40f7b1e25283 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 95609201da390..650a15e2e363b 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 3cecf3ee8eb30..d0e3423a47b3c 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 194b8ee4f16bf..185d30db3e650 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 87cddb00a8d7b..ad79eb6a01f91 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 006713618a6b8..e44d9341c9b8a 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 20adc65da111f..082b74478816b 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 ee0b8e56b6133..ec61f04a463b1 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 cc2a8d0f93e74..239da13d2257c 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/findModel.ts b/src/vs/editor/contrib/find/browser/findModel.ts index a4611d37dafa6..a6958ce4970e0 100644 --- a/src/vs/editor/contrib/find/browser/findModel.ts +++ b/src/vs/editor/contrib/find/browser/findModel.ts @@ -97,7 +97,12 @@ export class FindModelBoundToEditorModel { this._decorations = new FindDecorations(editor); this._toDispose.add(this._decorations); - this._updateDecorationsScheduler = new RunOnceScheduler(() => this.research(false), 100); + this._updateDecorationsScheduler = new RunOnceScheduler(() => { + if (!this._editor.hasModel()) { + return; + } + return this.research(false); + }, 100); this._toDispose.add(this._updateDecorationsScheduler); this._toDispose.add(this._editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index 303a9a58aaa02..9645d4b54ba9e 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -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(); 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 466c39baf1e6e..580ea739b7646 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 9bdb1cb77861e..49fef95baf81a 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 7d1ae5f5bd5b8..8cc753388b900 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 cc9e76c93b5b3..1f534bbdae300 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 f973d5f7a30dd..5f7ab05db78bb 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 993c8cf08272f..25988ac737061 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 03a9b4a402c26..2350f9aad343f 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 3cef0c2a56e0e..1ea615a5bd974 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 912cf515d1af0..a1a6dbe1dc19f 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 71281efa963c5..6d57dec4e783b 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 afee83dff2964..25db0e6d15d24 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 0a1c3220e6407..837dd6c0bc4bd 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 3551f44660662..3b2bdb0648e17 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 24ffac212bd5f..cf0e5c7d6d075 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/test/browser/referencesModel.test.ts b/src/vs/editor/contrib/gotoSymbol/test/browser/referencesModel.test.ts index d1195449779ff..a547f9450e1a6 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 81ad7b790edab..d79984f24c1a5 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -5,35 +5,30 @@ 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()); @@ -45,23 +40,27 @@ export class ContentHoverController extends Disposable implements IHoverWidget { @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); + 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(); + } - 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 = []; + private _initializeHoverParticipants(): IEditorHoverParticipant[] { + const participants: IEditorHoverParticipant[] = []; 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); + participants.push(participantInstance); } - this._participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal); - - this._computer = new ContentHoverComputer(this._editor, this._participants); - this._hoverOperation = this._register(new HoverOperation(this._editor, this._computer)); + 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 @@ -70,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 } })); @@ -92,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(); @@ -158,270 +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); } - - 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._doOnContentsChanged(), - 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)); - } + // 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 isBeforeContent = messages.some(m => m.isBeforeContent); - - if (statusBar.hasContent) { - fragment.appendChild(statusBar.hoverElement); + 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); + } - 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 _doOnContentsChanged(): void { - this._onContentsChanged.fire(); - this._widget.onContentsChanged(); + private _hideHover(): void { + this._contentHoverWidget.hide(); } - private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ - description: 'content-hover-highlight', - className: 'hoverHighlight' - }); - - 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) { + public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean { + const isContentWidgetResizing = this._contentHoverWidget.isResizing; + if (isContentWidgetResizing) { return true; } + 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) { - const anchor = participant.suggestHoverAnchor(mouseEvent); - if (anchor) { - anchorCandidates.push(anchor); - } + if (!participant.suggestHoverAnchor) { + continue; + } + const anchor = participant.suggestHoverAnchor(mouseEvent); + if (!anchor) { + continue; } + 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 - ) { + 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; } } - - if (anchorCandidates.length === 0) { - return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent); - } - 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 updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index?: number, focus?: boolean): Promise { - this._markdownHoverParticipant?.updateMarkdownHoverVerbosityLevel(action, index, focus); + public getWidgetContent(): string | undefined { + 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 focusedMarkdownHoverIndex(): number { - return this._markdownHoverParticipant?.focusedMarkdownHoverIndex() ?? -1; + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._renderedContentHover?.doesHoverAtIndexSupportVerbosityAction(index, action) ?? false; } - public markdownHoverContentAtIndex(index: number): string { - return this._markdownHoverParticipant?.markdownHoverContentAtIndex(index) ?? ''; + public getAccessibleWidgetContent(): string | undefined { + return this._renderedContentHover?.getAccessibleWidgetContent(); } - public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { - return this._markdownHoverParticipant?.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action) ?? false; + public getAccessibleWidgetContentAtIndex(index: number): string | undefined { + return this._renderedContentHover?.getAccessibleWidgetContentAtIndex(index); } - public getWidgetContent(): string | undefined { - const node = this._widget.getDomNode(); - if (!node.textContent) { - return undefined; - } - return node.textContent; + 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 { @@ -431,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 0000000000000..9308e6f295dbe --- /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 04d84593d06ee..bb43959419bdb 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 5d32dc8356070..a52d758baa72f 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 5890340091414..37cf8740e3721 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/hoverAccessibleViews.ts b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts index 2998c0e3e3084..41a7fbdfce992 100644 --- a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts +++ b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -18,15 +18,15 @@ 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, IDisposable } from 'vs/base/common/lifecycle'; +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 intro = localize('intro', "Focus on the hover widget to cycle through the hover parts with the Tab key."); - 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 const hoverContent = localize('contentHover', "The last focused hover content is the following."); + 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 { @@ -85,7 +85,6 @@ export class HoverAccessibilityHelp implements IAccessibleViewImplentation { } } - abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAccessibleViewContentProvider { abstract provideContent(): string; @@ -97,12 +96,9 @@ abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAc private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); public readonly onDidChangeContent: Event = this._onDidChangeContent.event; - protected _markdownHoverFocusedIndex: number = -1; - private _onHoverContentsChanged: IDisposable | undefined; + protected _focusedHoverPartIndex: number = -1; - constructor( - protected readonly _hoverController: HoverController, - ) { + constructor(protected readonly _hoverController: HoverController) { super(); } @@ -111,8 +107,8 @@ abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAc return; } this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = true; - this._markdownHoverFocusedIndex = this._hoverController.focusedMarkdownHoverIndex(); - this._onHoverContentsChanged = this._register(this._hoverController.onHoverContentsChanged(() => { + this._focusedHoverPartIndex = this._hoverController.focusedHoverPartIndex(); + this._register(this._hoverController.onHoverContentsChanged(() => { this._onDidChangeContent.fire(); })); } @@ -121,33 +117,39 @@ abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAc if (!this._hoverController) { return; } - this._markdownHoverFocusedIndex = -1; - this._hoverController.focus(); + if (this._focusedHoverPartIndex === -1) { + this._hoverController.focus(); + } else { + this._hoverController.focusHoverPartWithIndex(this._focusedHoverPartIndex); + } + this._focusedHoverPartIndex = -1; this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = false; - this._onHoverContentsChanged?.dispose(); - } -} - -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._markdownHoverFocusedIndex); + this.dispose(); } - provideContentAtIndex(index: number): string { - const content: string[] = []; - content.push(HoverAccessibilityHelpNLS.intro); - content.push(...this._descriptionsOfVerbosityActionsForIndex(index)); - content.push(...this._descriptionOfFocusedMarkdownHoverAtIndex(index)); - return content.join('\n'); + 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[] { @@ -164,7 +166,7 @@ export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvi } private _descriptionOfVerbosityActionForIndex(action: HoverVerbosityAction, index: number): string | undefined { - const isActionSupported = this._hoverController.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action); + const isActionSupported = this._hoverController.doesHoverAtIndexSupportVerbosityAction(index, action); if (!isActionSupported) { return; } @@ -175,15 +177,18 @@ export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvi return HoverAccessibilityHelpNLS.decreaseVerbosity; } } +} - protected _descriptionOfFocusedMarkdownHoverAtIndex(index: number): string[] { - const content: string[] = []; - const hoverContent = this._hoverController.markdownHoverContentAtIndex(index); - if (hoverContent) { - content.push('\n' + HoverAccessibilityHelpNLS.hoverContent); - content.push('\n' + hoverContent); - } - return content; +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); } } @@ -201,8 +206,7 @@ export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider } public provideContent(): string { - const hoverContent = this._hoverController.markdownHoverContentAtIndex(this._markdownHoverFocusedIndex); - return hoverContent.length > 0 ? hoverContent : this._hoverController.getWidgetContent() || HoverAccessibilityHelpNLS.intro; + return this.provideContentAtIndex(this._focusedHoverPartIndex, false); } public get actions(): IAction[] { @@ -229,16 +233,16 @@ export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider break; } const actionLabel = labelForHoverVerbosityAction(this._keybindingService, action); - const actionEnabled = this._hoverController.doesMarkdownHoverAtIndexSupportVerbosityAction(this._markdownHoverFocusedIndex, action); + const actionEnabled = this._hoverController.doesHoverAtIndexSupportVerbosityAction(this._focusedHoverPartIndex, action); return new Action(accessibleActionId, actionLabel, ThemeIcon.asClassName(actionCodicon), actionEnabled, () => { - editor.getAction(actionId)?.run({ index: this._markdownHoverFocusedIndex, focus: false }); + 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._markdownHoverFocusedIndex); }; + this.options.customHelp = () => { return helpProvider.provideContentAtIndex(this._focusedHoverPartIndex, true); }; } } diff --git a/src/vs/editor/contrib/hover/browser/hoverActions.ts b/src/vs/editor/contrib/hover/browser/hoverActions.ts index 20a5148fd742b..37eea40441ae8 100644 --- a/src/vs/editor/contrib/hover/browser/hoverActions.ts +++ b/src/vs/editor/contrib/hover/browser/hoverActions.ts @@ -432,7 +432,12 @@ export class IncreaseHoverVerbosityLevel extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { - HoverController.get(editor)?.updateMarkdownHoverVerbosityLevel(HoverVerbosityAction.Increase, args?.index, args?.focus); + const hoverController = HoverController.get(editor); + if (!hoverController) { + return; + } + const index = args?.index !== undefined ? args.index : hoverController.focusedHoverPartIndex(); + hoverController.updateHoverVerbosityLevel(HoverVerbosityAction.Increase, index, args?.focus); } } @@ -448,6 +453,11 @@ export class DecreaseHoverVerbosityLevel extends EditorAction { } public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void { - HoverController.get(editor)?.updateMarkdownHoverVerbosityLevel(HoverVerbosityAction.Decrease, args?.index, args?.focus); + 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/hoverController.ts b/src/vs/editor/contrib/hover/browser/hoverController.ts index 293902db46d36..b6ef34860ca52 100644 --- a/src/vs/editor/contrib/hover/browser/hoverController.ts +++ b/src/vs/editor/contrib/hover/browser/hoverController.ts @@ -417,26 +417,26 @@ export class HoverController extends Disposable implements IEditorContribution { return this._contentWidget?.widget.isResizing || false; } - public focusedMarkdownHoverIndex(): number { - return this._getOrCreateContentWidget().focusedMarkdownHoverIndex(); + public focusedHoverPartIndex(): number { + return this._getOrCreateContentWidget().focusedHoverPartIndex(); } - public markdownHoverContentAtIndex(index: number): string { - return this._getOrCreateContentWidget().markdownHoverContentAtIndex(index); + public doesHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { + return this._getOrCreateContentWidget().doesHoverAtIndexSupportVerbosityAction(index, action); } - public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean { - return this._getOrCreateContentWidget().doesMarkdownHoverAtIndexSupportVerbosityAction(index, action); - } - - public updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index?: number, focus?: boolean): void { - this._getOrCreateContentWidget().updateMarkdownHoverVerbosityLevel(action, index, focus); + 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(); } @@ -473,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 68483d2bbfa3f..4b23a83847876 100644 --- a/src/vs/editor/contrib/hover/browser/hoverTypes.ts +++ b/src/vs/editor/contrib/hover/browser/hoverTypes.ts @@ -94,7 +94,22 @@ export interface IEditorHoverColorPickerWidget { layout(): void; } -export interface IEditorHoverRenderContext { +export interface IEditorHoverContext { + /** + * The contents rendered inside the fragment have been changed, which means that the hover should relayout. + */ + onContentsChanged(): void; + /** + * Set the minimum dimensions of the resizable hover + */ + setMinimumDimensions?(dimensions: Dimension): void; + /** + * Hide the hover. + */ + hide(): void; +} + +export interface IEditorHoverRenderContext extends IEditorHoverContext { /** * The fragment where dom elements should be attached. */ @@ -103,22 +118,38 @@ export interface IEditorHoverRenderContext { * The status bar for actions for this hover. */ readonly statusBar: IEditorHoverStatusBar; +} + +export interface IRenderedHoverPart extends IDisposable { /** - * Set if the hover will render a color picker widget. - */ - setColorPicker(widget: IEditorHoverColorPickerWidget): void; - /** - * The contents rendered inside the fragment have been changed, which means that the hover should relayout. + * The rendered hover part. */ - onContentsChanged(): void; + hoverPart: T; /** - * Set the minimum dimensions of the resizable hover + * The HTML element containing the hover part. */ - setMinimumDimensions?(dimensions: Dimension): void; + hoverElement: HTMLElement; +} + +export interface IRenderedHoverParts extends IDisposable { /** - * Hide the hover. + * Array of rendered hover parts. */ - hide(): void; + 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 { @@ -127,7 +158,9 @@ export interface IEditorHoverParticipant { 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 1adf0bbe0b8d4..aaeb5796aa61a 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,55 +195,65 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant { + return Promise.resolve(this._renderedHoverParts?.updateMarkdownHoverPartVerbosityLevel(action, index, focus)); } } -interface RenderedHoverPart { - renderedMarkdown: HTMLElement; - disposables: DisposableStore; - hoverSource?: HoverSource; +class RenderedMarkdownHoverPart implements IRenderedHoverPart { + + constructor( + 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 extends Disposable { +class MarkdownRenderedHoverParts implements IRenderedHoverParts { + + public renderedHoverParts: RenderedMarkdownHoverPart[]; - private _renderedHoverParts: RenderedHoverPart[]; - private _focusedHoverPartIndex: number = -1; private _ongoingHoverOperations: Map = new Map(); + private readonly _disposables = new DisposableStore(); + constructor( - hoverParts: MarkdownHover[], // we own! + 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); }); })); } @@ -247,75 +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)); - - this._register(dom.addDisposableListener(renderedMarkdown, dom.EventType.FOCUS_IN, (event: Event) => { - event.stopPropagation(); - this._focusedHoverPartIndex = hoverPartIndex; - })); - this._register(dom.addDisposableListener(renderedMarkdown, dom.EventType.FOCUS_OUT, (event: Event) => { - event.stopPropagation(); - this._focusedHoverPartIndex = -1; - })); - 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 { @@ -324,59 +320,74 @@ 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); - store.add(this._hoverService.setupUpdatableHover(hoverDelegate, actionElement, labelForHoverVerbosityAction(this._keybindingService, action))); + 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.updateMarkdownHoverPartVerbosityLevel(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 updateMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction, index: number = -1, focus: boolean = true): 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 indexOfInterest = index !== -1 ? index : this._focusedHoverPartIndex; - const hoverRenderedPart = this._getRenderedHoverPartAtIndex(indexOfInterest); - 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( - indexOfInterest, + 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(indexOfInterest, newHoverRenderedPart); + this._replaceRenderedHoverPartAtIndex(index, newHoverRenderedPart, newHoverPart); if (focus) { - this._focusOnHoverPartWithIndex(indexOfInterest); + this._focusOnHoverPartWithIndex(index); } - this._onFinishedRendering(); - } - - public markdownHoverContentAtIndex(index: number): string { - const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index); - return hoverRenderedPart?.renderedMarkdown.innerText ?? ''; + return { + hoverPart: newHoverPart, + hoverElement: newHoverRenderedPart.hoverElement + }; } - public focusedMarkdownHoverIndex(): number { - return this._focusedHoverPartIndex; + 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); - if (!hoverRenderedPart || !hoverRenderedPart.hoverSource?.supportsVerbosityAction(action)) { + const hoverSource = hoverRenderedPart?.hoverPart.source; + if (!hoverRenderedPart || !hoverSource?.supportsVerbosityAction(action)) { return false; } return true; @@ -404,76 +415,94 @@ 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.renderedHoverParts[index].hoverElement.focus(); + } + + private _getRenderedHoverPartAtIndex(index: number): RenderedMarkdownHoverPart | undefined { + return this.renderedHoverParts[index]; } - private _getRenderedHoverPartAtIndex(index: number): RenderedHoverPart | 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); } - return store; + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: markdownHover, + hoverElement: renderedMarkdown, + dispose() { disposables.dispose(); } + }; + return renderedHoverPart; } export function labelForHoverVerbosityAction(keybindingService: IKeybindingService, action: HoverVerbosityAction): string { diff --git a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts index 3ed0b3fab14fd..86dba6a80e3f3 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); + } + + public getAccessibleContent(hoverPart: MarkerHover): string { + return hoverPart.marker.message; } - private renderMarkerHover(markerHover: MarkerHover, disposables: DisposableStore): HTMLElement { + 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 b41a164ec86ed..65762f5905b41 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 f7d859c5d5a63..84760fdb6f448 100644 --- a/src/vs/editor/contrib/indentation/browser/indentation.ts +++ b/src/vs/editor/contrib/indentation/browser/indentation.ts @@ -418,6 +418,10 @@ export class AutoIndentOnPaste implements IEditorContribution { if (!model) { return; } + const containsOnlyWhitespace = this.rangeContainsOnlyWhitespaceCharacters(model, range); + if (containsOnlyWhitespace) { + return; + } if (isStartOrEndInString(model, range)) { return; } @@ -466,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); @@ -546,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); 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 d6adda566d8c3..2beed4f823be3 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,7 +22,10 @@ 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'; export enum Language { TypeScript = 'ts-test', @@ -44,16 +47,11 @@ function testIndentationToTabsCommand(lines: string[], selection: Selection, tab testCommand(lines, null, selection, (accessor, sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection); } -export 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 }); } -export 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 @@ export function registerLanguageConfiguration(instantiationService: TestInstanti lineComment: '//', blockComment: ['/*', '*/'] }, + autoClosingPairs: javascriptAutoClosingPairsRules, indentationRules: javascriptIndentationRules, onEnterRules: javascriptOnEnterRules }); @@ -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) ', @@ -569,7 +581,7 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { const model = createTextModel(initialText, 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 }, @@ -582,7 +594,6 @@ suite('Auto Indent On Paste - TypeScript/JavaScript', () => { { startIndex: 0, standardTokenType: StandardTokenType.String }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); editor.setSelection(new Selection(2, 10, 2, 15)); @@ -602,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 {', @@ -634,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'); @@ -654,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 = [ '() => {', @@ -662,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)); @@ -696,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 = [ '() => {', @@ -704,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 @@ -727,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) {', @@ -735,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 @@ -760,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 }, @@ -791,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)); @@ -799,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)); @@ -817,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(() => { @@ -837,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(), [ @@ -859,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(), [ @@ -879,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;', @@ -908,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(), [ @@ -939,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(), @@ -970,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(), @@ -996,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(), [ @@ -1020,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(), [ @@ -1055,9 +1059,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, instantiationService) => { const tokens: StandardTokenTypeData[][] = [ [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }], [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }], @@ -1075,6 +1077,38 @@ 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', () => { @@ -1090,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(), [ @@ -1114,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(), [ @@ -1138,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("."); @@ -1163,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(), [ @@ -1189,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(), [ @@ -1213,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(), [ @@ -1233,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(), [ @@ -1267,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(), [ @@ -1295,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(), [ @@ -1319,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(), [ @@ -1350,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(), [ @@ -1366,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(() => { @@ -1384,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"); @@ -1412,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(), [ @@ -1430,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(() => { @@ -1448,9 +1481,7 @@ suite('Auto Indent On Type - PHP', () => { 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 }, @@ -1473,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(() => { @@ -1500,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); @@ -1521,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(() => { @@ -1546,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(), [ @@ -1569,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(), [ @@ -1591,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(), [ @@ -1609,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(() => { @@ -1635,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(), [ @@ -1653,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(() => { @@ -1679,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(), [ @@ -1697,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(() => { @@ -1722,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(), [ @@ -1738,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(() => { @@ -1762,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(), [ @@ -1773,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 index 77afbe5c76d40..2004b864cbe79 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentationLineProcessor.test.ts @@ -2,24 +2,39 @@ * 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 { 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, registerTokenizationSupport, StandardTokenTypeData } from 'vs/editor/contrib/indentation/test/browser/indentation.test'; +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(() => { @@ -35,13 +50,12 @@ suite('Indentation Context Processor - 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 }, { startIndex: 16, standardTokenType: StandardTokenType.String }, { startIndex: 28, standardTokenType: StandardTokenType.String } ]]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); @@ -60,7 +74,7 @@ suite('Indentation Context Processor - 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 }, @@ -72,7 +86,6 @@ suite('Indentation Context Processor - TypeScript/JavaScript', () => { { startIndex: 46, standardTokenType: StandardTokenType.Other }, { startIndex: 47, standardTokenType: StandardTokenType.String } ]]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); @@ -91,7 +104,7 @@ suite('Indentation Context Processor - 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 }, @@ -104,7 +117,6 @@ suite('Indentation Context Processor - TypeScript/JavaScript', () => { { startIndex: 44, standardTokenType: StandardTokenType.Other }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService); @@ -120,9 +132,20 @@ 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(() => { @@ -140,7 +163,7 @@ suite('Processed Indent Rules Support - 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 } @@ -154,7 +177,6 @@ suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { { startIndex: 17, standardTokenType: StandardTokenType.Comment }, ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; @@ -177,13 +199,12 @@ suite('Processed Indent Rules Support - 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 }], [{ startIndex: 0, standardTokenType: StandardTokenType.String }], [{ startIndex: 0, standardTokenType: StandardTokenType.Comment }] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; @@ -206,7 +227,7 @@ suite('Processed Indent Rules Support - 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 } @@ -220,7 +241,6 @@ suite('Processed Indent Rules Support - TypeScript/JavaScript', () => { { startIndex: 18, standardTokenType: StandardTokenType.RegEx } ] ]; - disposables.add(registerLanguage(instantiationService, languageId)); disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); const indentationRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport; diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsHover.ts index d49a4bb781fa3..05703e108d180 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 0c93d8465ab94..450f9549fb78f 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 90d4ffcba0c12..7bcf0ea59f1f4 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); + disposables.add(w); + const renderedHoverPart: IRenderedHoverPart = { + hoverPart: part, + hoverElement: widgetNode, + dispose() { disposables.dispose(); } + }; + return new RenderedHoverParts([renderedHoverPart]); + } - return disposableStore; + getAccessibleContent(hoverPart: InlineCompletionsHover): string { + return nls.localize('hoverAccessibilityStatusBar', 'There are inline completions here'); } - private renderScreenReaderText(context: IEditorHoverRenderContext, part: InlineCompletionsHover, disposableStore: DisposableStore) { + 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 c11920c4d65d5..a7c25e42677cf 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 _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) => { - return store.add(this._instantiationService.createInstance(GhostTextWidget, this.editor, { + 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 _debounceValue = this._debounceService.for( - this._languageFeaturesService.inlineCompletionsProvider, - 'InlineCompletionsDebounce', - { min: 50, max: 50 } - ); + })) + ).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(cancellationStore)); - await waitForState(this._suggestWidgetAdaptor.selectedItem, isUndefined, () => false, cancelOnDispose(cancellationStore)); - - await this._accessibilitySignalService.playSignal(AccessibilitySignal.inlineSuggestion); + await timeout(50, cancelOnDispose(store)); + await waitForState(this._suggestWidgetSelectedItem, isUndefined, () => false, cancelOnDispose(store)); - 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 9f7d3d17ba88f..2226a1ea26500 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 bc10c23e4917f..92013fd598282 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'; @@ -23,6 +23,7 @@ import { Command, InlineCompletionContext, InlineCompletionTriggerKind, PartialA 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'; @@ -32,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); @@ -54,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, @@ -91,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: () => ({ @@ -99,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; @@ -114,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); @@ -264,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; @@ -320,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( @@ -341,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 @@ -458,9 +452,7 @@ export class InlineCompletionsModel extends Disposable { completion.source.inlineCompletions, completion.sourceInlineCompletion, text.length, - { - kind, - } + { kind, } ); } } finally { @@ -485,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 @@ -527,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 32b77dd23fe5e..1eb06aa11c1f0 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 28052040c3296..25ccf4cd12598 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 95d135ce324e5..daf73173ef438 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 648f594059677..63ab19affbe24 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 e160c3daa015c..e318702da15be 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 354fa36b5af81..a860898c64e93 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 298fcd4452e1f..9a4c3a927b4f3 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 6c1c7337f7fc5..cfacc77329a62 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 7196773a7cfc2..0be0d1254c005 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 d6d156544e00e..aced5d6271e96 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 283e72be068b3..4e231fd5162b4 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 ee0b537a88cba..cb2453ab6c675 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/src/vs/workbench/workbench.web.main.nls.js b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.css similarity index 58% rename from src/vs/workbench/workbench.web.main.nls.js rename to src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.css index 6113d093d5cce..bc7e553e4d856 100644 --- a/src/vs/workbench/workbench.web.main.nls.js +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.css @@ -3,4 +3,10 @@ * 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 +.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; +} 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 0000000000000..bd10e0669b4cb --- /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 0000000000000..ec7255fd9f02d --- /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 0000000000000..9ad19e98a7655 --- /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 0000000000000..ae8b7182a89e1 --- /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 0000000000000..9055ec5671932 --- /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 0000000000000..812818c85b1a2 --- /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 0000000000000..68910c883a69f --- /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 0000000000000..331520cffe0b9 --- /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 db892ea676c28..25f273c126288 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 4ef8e7d2cfc3b..17b47434db7f6 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 45b3fafba7146..60601d1fcdd71 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 68614a2f432f9..9a4f6800938f7 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 c53496fb5de3a..48348ae969034 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 5425697a2e4b6..f99fb62422de5 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 982dc07426b82..80d6ed9373039 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 64de23fce27fa..008150e7c09cf 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 f5b8af528e3d5..3036277c4487e 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 6c84215bc72df..10372b408c4b7 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 a0f2dfd914ed5..86f6cb1d47f1d 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 0000000000000..e9994c4b01c2d --- /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 c42b21a3ba28c..043b6f156327d 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 52c0edd1cb8c3..f048991d0f9d3 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); + + private readonly _view = derivedWithStore((reader, store) => { + if (!this._shouldViewBeAlive.read(reader)) { return; } - return [{ - range: new Range(1, 1, 1, 1), - options: { - description: 'placeholder', - showIfCollapsed: true, - after: { - content: options.placeholder, - cursorStops: InjectedTextCursorStops.None, - inlineClassName: 'placeholder-text' - } - } - }]; + 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/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts index 46aeeda1706c1..d34aeb3faf515 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts @@ -48,7 +48,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit 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; diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.ts b/src/vs/editor/contrib/rename/browser/renameWidget.ts index 03257f28aa3ab..b284cd519fde0 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 c2d9a57c2026f..86d3bf5d62dca 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 da8dc2f222d8d..6ac97ecde9616 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 dc9e9767d8183..1de9179cc2823 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 e0f11350e8943..236ad70f27d82 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 1c9c3a80cdbe5..5b9a5e434f8fc 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 ee2f1f1d24f9c..bbf24168409d1 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 6106184672439..0a9a3c76462da 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 53a9b272757af..b8ea4e5234255 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 3bc52c6c9157f..276082256f6ef 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 bdc0542b18e54..22b8c67a2942e 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 d0e8da4b17a13..27d3d8e1a58e7 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -35,6 +35,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 }); @@ -126,7 +130,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))) ) { @@ -205,7 +209,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; @@ -258,7 +262,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; @@ -358,7 +362,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 04e852765caed..aaefcb65f3dc6 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 2eeb94d99b610..2d30e5925ab13 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 25d19c054d254..4a1df5b9ce868 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 cb08e56fb7f99..823d6865a84ae 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 d4df28ea332bc..b2e9fc0357754 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 617019ff5cfb7..64123de47db74 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 1aaca84d48411..91d42374b1fcb 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 3bbc8c5883869..57d0d030d4879 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 2bbfe524bfb15..5c673c0f35562 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 04f7bce1cc1c3..15f238b162743 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 64ccb1bc61897..7d6fe4b73f6a7 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_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_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', 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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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 f44fc76e0bdf8..eae47ac2d717a 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 b3825b0f05d73..79ec7ad02e684 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 388d42021d227..29381ecf63fc6 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 a06bf07200a57..d90d44fd2d6a8 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 ebcabc415a474..5ed4d310941b7 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 12258f71cfb67..b3a72759a0460 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 8c50e8dcf3623..e40c7056aa7ad 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 5e15b129e0e8c..9682a3cfb0f39 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 e9f5f3934c45b..599c89a021384 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 12feefa27d5f6..fbe58b6216962 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 92e385564acac..745c0075541cf 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 e44de083b985e..649783848e340 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 a769b21c4f9fc..8d98e80365847 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 6cdb2657aa7a4..fbd9a36c95a4a 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 5b4d0994a62dc..15f9bb999d43d 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 b507c54686f8f..911b73018c30e 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 d44cc97a7f4b8..a2e3ace58f6d8 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'; diff --git a/src/vs/editor/test/browser/controller/cursor.integrationTest.ts b/src/vs/editor/test/browser/controller/cursor.integrationTest.ts index 6373a6fd60389..bd956a353981f 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 a86e95c1303da..bb2abffb9c99b 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 2e312c53e3ea3..c4bee4b9f2b2f 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 19fcf3b1970b6..fd2e23e375155 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 087e99311f281..2baa9ddda39e3 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 b8eb27e6d0fef..9160c07070a72 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 26dea91e68196..d732776a8009a 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 60abb6d301f6d..e12d2cc526a8a 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 ab0d682a7be83..4fcfa5cf3cddd 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 0fcb0b1d57d1f..0f9588d870c01 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 0974ab8d183c5..292aa0ed1fa6c 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 fe7f0c6e2dbf9..36749b71bd667 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 02786c056b3d2..a89fc01e245dd 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 4b515f974b292..ff16b570be11e 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 192b4b6d8e49a..a09d5c98f1807 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 1f08e089c2e59..53ecda34e134b 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 0000000000000..0a10496647023 --- /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 200cb5e226afb..9c2efda4f77ec 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 c2d8dc85a45bd..e90bdd5a5714d 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 dd4c4b02df3d1..4271d91f92318 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 b67b9bdfb7baf..1b45b3f282909 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 177e66774df01..d2457fa2bc8ea 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 39aead0a84852..1811a08e90f32 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 bf574592d4ea1..fcbb0cd0fccf3 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 af90a1f5eb166..6afe99db33a0e 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/textEdit.test.ts b/src/vs/editor/test/common/core/textEdit.test.ts index f02e8a9bd5036..4458eaf8a5b20 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 38378ef38263d..651dc5a79f205 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 264292f82ff07..514556238b536 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 2a21907778def..1c2ea8c3b49e7 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 9155b32a9cae5..a1def431d2b7d 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 42d0eae8f81cd..6fced6cef3e53 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 09c9e34a124f4..87215503dc4b0 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 eb49c1048d118..57086d81ff884 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 45d4dcc68337a..2b5026e4f0cfc 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 a83b30a66eb42..f306d99ccabc3 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 d220d12e668e0..da409c9d8a3c4 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 aa39ad03e8c49..d2af9519432d5 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 06803315a7b9e..2228a6174a459 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 06534b6e2a667..dd2fd332be548 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 77bc1e74cc16b..a4604874d657c 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 22f182090b8ed..662ef1fe8fde9 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 5eb7462e045fa..91279c1d70d1e 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 fa48350404f4e..66df53d83df81 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 a75e86e7b59c6..e699174a650d2 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 dda14417b1acf..c00d0ce8f2a96 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 a2cd24ce4f473..0aeebe90c6f52 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 72d0f9ba0a3a2..f7c023c4f8a86 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 007033dc79072..01cc7cb6ef60f 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 164e7ff92e133..a58430b309f98 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 dc6037d6df4d0..3270a56338618 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 91ec41810f343..0f03a1e0730ba 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 b425cc30fbf49..34171ea9b1de3 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 cdb8528819bf5..54ef6b8d6283d 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 7ceba9ef59862..f4e9413a422c5 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 97c74722cc9d7..28e6340d9f3d8 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 ce3aa3f407800..3de1b762b5936 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 49411db88e964..2d769837672df 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 968bd3508926c..0f5ebc499bd24 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 70c763ec16efa..e92b7db2e6e11 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 90d89185aa48d..20170cb8f4850 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 1daa14e160745..4ed40eb1abbbe 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 f58f7105d7eb1..5fd90a7156101 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 26854898f0364..b5386ec1b3bb9 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 7593ea14706e7..af7015a19b6c3 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 5dc2dd11f16e6..781ff45021109 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 b0b54a96746e3..e8317e6ae881f 100644 --- a/src/vs/editor/test/common/services/languageService.test.ts +++ b/src/vs/editor/test/common/services/languageService.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { LanguageService } from 'vs/editor/common/services/languageService'; diff --git a/src/vs/editor/test/common/services/languagesAssociations.test.ts b/src/vs/editor/test/common/services/languagesAssociations.test.ts index 689457fe6788c..7d6ce3ba4dca4 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 74ec1559d437f..d4715b8534c04 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 53eb701ecfbc9..cd4b53d86df55 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 b32e7e66c743a..7093691179dd5 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 bfec2c00b1ad8..1128768e91911 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 11410512dc631..c7fbd1afff21b 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 b646a4c18bea9..9b5351bd6189e 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 5896b2a928b07..b488141d7d7da 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 ecfde1e3d915c..d1058c422939d 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 c3f0fa635ab57..58f9217ea56af 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 2fdbeaefdf82e..22e1c60f7af1d 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 7c0522a84f0d5..84659a045c60c 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 85792a4239099..b771b7e5ff8b4 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 37727d4c8a1cd..a1defd0c4f54c 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 1fdb7a2c10bd8..e1820e6e920bf 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 e9e8d3fec1a21..d2657a7ce4122 100644 --- a/src/vs/editor/test/node/classification/typescript.test.ts +++ b/src/vs/editor/test/node/classification/typescript.test.ts @@ -3,7 +3,7 @@ * Licensed 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'; diff --git a/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts b/src/vs/editor/test/node/diffing/defaultLinesDiffComputer.test.ts index 995472ca78f5b..72ad7fe2186b4 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 e944d133befbf..0ed9a8b11fe55 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 bdaa293acd473..b9b792ab06865 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 0000000000000..db510b7563551 --- /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 0000000000000..0dc735e1c5ac9 --- /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 0000000000000..181c78999fa59 --- /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 0000000000000..727c2e8eb55f6 --- /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 0000000000000..7d4c141530883 --- /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 0000000000000..9b3687d776a96 --- /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 0000000000000..06f0ca747cf93 --- /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 0000000000000..88d6c0c6cf8f1 --- /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 ea2f3c43a06a7..584faab47accf 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. @@ -4566,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 { @@ -4929,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: { @@ -5003,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; @@ -5016,7 +5021,7 @@ declare namespace monaco.editor { codeLensFontFamily: IEditorOption; codeLensFontSize: IEditorOption; colorDecorators: IEditorOption; - colorDecoratorActivatedOn: IEditorOption; + colorDecoratorActivatedOn: IEditorOption; colorDecoratorsLimit: IEditorOption; columnSelection: IEditorOption; comments: IEditorOption>>; @@ -5083,6 +5088,7 @@ declare namespace monaco.editor { pasteAs: IEditorOption>>; parameterHints: IEditorOption>>; peekWidgetDefaultFocus: IEditorOption; + placeholder: IEditorOption; definitionLinkOpensInPeek: IEditorOption; quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; @@ -5124,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; @@ -7939,6 +7945,11 @@ declare namespace monaco.languages { arguments?: any[]; } + export interface CommentThreadRevealOptions { + preserveFocus: boolean; + focusReply: boolean; + } + export interface CommentAuthorInformation { name: string; iconPath?: UriComponents; @@ -8053,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 05c063fa291a3..0000000000000 --- 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 5323c6c6340d8..0000000000000 --- 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 233840e65abff..5a546325fc7a2 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; -} +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 { + message = originalMessage; + } -/** - * @skipMangle - */ -export function setPseudoTranslation(value: boolean) { - isPseudo = value; -} + const value = _format(message, args); -/** - * 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) + value, + original: originalMessage === message ? value : _format(originalMessage, args) }; } -/** - * 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); - } - } - })(); - } 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); - }); - } -} +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 bd84abbc6dc9f..408fbc07b2ae2 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/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 65515c237adae..51d518e00f87f 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 e7b26382eaeba..20a030b67ab5f 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 165caec0bf1f4..530b1fe617003 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 e6840b10e1010..e7285146aa093 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 c5cba140485b7..7eb35af7e4b00 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 da596a8fc6c02..58904af44aa1d 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 4857d4bc07bfc..003ee6e781c0e 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 fa9bd76ed8a5e..eb9a7babc2711 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -138,10 +138,12 @@ 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'); static readonly TestPeekTitle = new MenuId('TestPeekTitle'); + static readonly TestCallStackContext = new MenuId('TestCallStackContext'); static readonly TouchBarContext = new MenuId('TouchBarContext'); static readonly TitleBarContext = new MenuId('TitleBarContext'); static readonly TitleBarTitleContext = new MenuId('TitleBarTitleContext'); @@ -171,6 +173,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 +213,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 +276,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 +293,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 +302,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 61fefa8e4ae30..91d9ffdc49dad 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 31e5d6c17be59..8a3b2c421a906 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 9fd5c04506540..5ff0da22ae5bb 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 3e56af6472007..5e7e71cdd7a4c 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 ed5490e783f74..d22b7bb5bc0eb 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -134,7 +134,7 @@ export class BrowserClipboardService extends Disposable implements IClipboardSer 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 eeb3ed5da2af2..f46f8e1a6ae71 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 8b862b1c93908..553c312d6be3f 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 ed8e56d50c5a9..7876af5831eca 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,33 +415,149 @@ 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); + } + } + } + + 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 { // defaults: remove let defaultsOverrides = false; @@ -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 d6f09a4d17613..5e3c303ada6f0 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 e7b169e734188..1f709cf2c0fd6 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 50b612d83d900..ab425c4fd30f4 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 9fc9e709322bf..0f3bf7e2d87c5 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 880e49e8e48c7..0eff504b9bd16 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 378107be505f9..5b237b7371940 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 94ee037eb5d81..d3e44993618ec 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 b56d4b874da1e..d2301c19147e5 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 8f388fb13ee55..2555701c1d809 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 c5be259634180..17bfa468ec9eb 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 df897db9e8a5d..dacbfbebbdd05 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/contextMenuService.ts b/src/vs/platform/contextview/browser/contextMenuService.ts index 907cf9449a89a..f70e349945e67 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/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 26a715bfc19cf..7e0bc1170e0dd 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'; @@ -178,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); diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index bc1230a48ea61..2fffe78c5fb66 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/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index 748ff075783dd..dec04406afaf3 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 26b7d9c693745..16a942afe0596 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 d8cefb6df673d..a94fca911ea2b 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 56ab151407d42..b3e71e04493d4 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 92898523ed111..3661c50a3ba4f 100644 --- a/src/vs/platform/environment/node/userDataPath.js +++ b/src/vs/platform/environment/node/userDataPath.js @@ -6,12 +6,22 @@ /// //@ts-check +'use strict'; + +// ESM-uncomment-begin +// import * as os from 'os'; +// import * as path from 'path'; +// +// const module = { exports: {} }; +// ESM-uncomment-end + (function () { - '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 @@ -115,11 +125,17 @@ return factory(path, os, process.cwd()); // amd }); } else if (typeof module === 'object' && typeof module.exports === 'object') { + // ESM-comment-begin const path = require('path'); const os = require('os'); + // ESM-comment-end module.exports = factory(path, os, process.env['VSCODE_CWD'] || process.cwd()); // commonjs } else { throw new Error('Unknown context'); } }()); + +// ESM-uncomment-begin +// export const getUserDataPath = module.exports.getUserDataPath; +// ESM-uncomment-end 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 78fd73545201a..268f5ce52bb76 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 a188b52b71130..a82be9607d068 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 ffe418fc70263..6f256621040aa 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 68128a214fd5e..d2997c2d22b9f 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(); @@ -56,7 +56,12 @@ flakySuite('Native Modules (all platforms)', () => { }); test('@vscode/sqlite3', async () => { + // ESM-comment-begin const sqlite3 = await import('@vscode/sqlite3'); + // ESM-comment-end + // ESM-uncomment-begin + // const { default: sqlite3 } = await import('@vscode/sqlite3'); + // ESM-uncomment-end assert.ok(typeof sqlite3.Database === 'function', testErrorMessage('@vscode/sqlite3')); }); diff --git a/src/vs/platform/environment/test/node/userDataPath.test.ts b/src/vs/platform/environment/test/node/userDataPath.test.ts index 644260cff8ff3..72278e46ac0f4 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 4d9e5a7a30597..6768621e2cd67 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -23,6 +23,7 @@ import { } 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'; @@ -195,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(); @@ -206,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: [] }); @@ -548,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); @@ -774,7 +797,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract getTargetPlatform(): Promise; abstract zip(extension: ILocalExtension): Promise; - abstract unzip(zipLocation: URI): Promise; abstract getManifest(vsix: URI): Promise; abstract install(vsix: URI, options?: InstallOptions): Promise; abstract installFromLocation(location: URI, profileLocation: URI): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 5548b8eefb94a..e8698264bfabe 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) { @@ -1137,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); @@ -1161,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 */ } @@ -1262,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 }); @@ -1293,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 fa3ad5f55955b..08915a062ab25 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; @@ -437,6 +439,7 @@ export const enum ExtensionManagementErrorCode { Deprecated = 'Deprecated', Malicious = 'Malicious', Incompatible = 'Incompatible', + IncompatibleApi = 'IncompatibleApi', IncompatibleTargetPlatform = 'IncompatibleTargetPlatform', ReleaseVersionNotFound = 'ReleaseVersionNotFound', Invalid = 'Invalid', @@ -487,6 +490,7 @@ export type InstallOptions = { profileLocation?: URI; installOnlyNewlyAddedFromExtensionPack?: boolean; productVersion?: IProductVersion; + keepExisting?: boolean; /** * Context passed through to InstallExtensionResult */ @@ -519,7 +523,6 @@ export interface IExtensionManagementService { onDidUpdateExtensionMetadata: Event; zip(extension: ILocalExtension): Promise; - unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; install(vsix: URI, options?: InstallOptions): Promise; canInstall(extension: IGalleryExtension): Promise; @@ -534,6 +537,7 @@ export interface IExtensionManagementService { getExtensionsControlManifest(): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: 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 9241f4170ab8c..064420130d1dd 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: installOptions.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 75cf3eb91aee1..c0d8aef3272d4 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -109,9 +109,6 @@ export class ExtensionManagementChannel implements IServerChannel { const uri = await this.service.zip(extension); return transformOutgoingURI(uri, uriTransformer); } - case 'unzip': { - return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); - } case 'install': { return this.service.install(transformIncomingURI(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer)); } @@ -158,6 +155,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(); } @@ -238,10 +238,6 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(result))); } - unzip(zipLocation: URI): Promise { - return Promise.resolve(this.channel.call('unzip', [zipLocation])); - } - install(vsix: URI, options?: InstallOptions): Promise { return Promise.resolve(this.channel.call('install', [vsix, options])).then(local => transformIncomingExtension(local, null)); } @@ -289,6 +285,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 e92897f174f9e..7d4c51d21bbaf 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 { @@ -713,7 +726,7 @@ class ExtensionsScanner extends Disposable { return null; } if (getNodeType(manifest) !== 'object') { - this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", manifestLocation.path))); + this.logService.error(this.formatMessage(extensionLocation, localize('jsonParseInvalidType', "Invalid manifest file {0}: Not a JSON object.", manifestLocation.path))); return null; } return manifest; @@ -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 0c13f71235c19..ba6c8c81533e1 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'; @@ -110,12 +111,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return URI.file(location); } - async unzip(zipLocation: URI): Promise { - this.logService.trace('ExtensionManagementService#unzip', zipLocation.toString()); - const local = await this.install(zipLocation); - return local.identifier; - } - async getManifest(vsix: URI): Promise { const { location, cleanup } = await this.downloadVsix(vsix); const zipPath = path.resolve(location.fsPath); @@ -353,7 +348,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi pinned: options.installGivenVersion ? true : !!options.pinned, source: 'vsix', }, - true, + options.keepExisting ?? true, token); return { local }; } @@ -363,7 +358,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 +479,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 +500,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 +527,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))); @@ -552,69 +576,67 @@ export class ExtensionsScanner extends Disposable { const tempLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`)); const extensionLocation = URI.file(path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName)); - let exists = await this.fileService.exists(extensionLocation); + if (await this.fileService.exists(extensionLocation)) { + if (!removeIfExists) { + try { + return await this.scanLocalExtension(extensionLocation, ExtensionType.User); + } catch (error) { + this.logService.warn(`Error while scanning the existing extension at ${extensionLocation.path}. Deleting the existing extension and extracting it.`, getErrorMessage(error)); + } + } - if (exists && removeIfExists) { try { await this.deleteExtensionFromLocation(extensionKey.id, extensionLocation, 'removeExisting'); } catch (error) { throw new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionLocation.fsPath, extensionKey.id), ExtensionManagementErrorCode.Delete); } - exists = false; } - if (exists) { + try { + if (token.isCancellationRequested) { + throw new CancellationError(); + } + + // Extract try { - await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); + this.logService.trace(`Started extracting the extension from ${zipPath} to ${extensionLocation.fsPath}`); + await extract(zipPath, tempLocation.fsPath, { sourcePath: 'extension', overwrite: true }, token); + this.logService.info(`Extracted extension to ${extensionLocation}:`, extensionKey.id); + } catch (e) { + throw fromExtractError(e); + } + + 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); } - } else { - try { - if (token.isCancellationRequested) { - throw new CancellationError(); - } - // Extract - try { - this.logService.trace(`Started extracting the extension from ${zipPath} to ${extensionLocation.fsPath}`); - await extract(zipPath, tempLocation.fsPath, { sourcePath: 'extension', overwrite: true }, token); - this.logService.info(`Extracted extension to ${extensionLocation}:`, extensionKey.id); - } catch (e) { - throw fromExtractError(e); - } - - try { - await this.extensionsScannerService.updateMetadata(tempLocation, metadata); - } catch (error) { - throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); - } - - if (token.isCancellationRequested) { - throw new CancellationError(); - } + if (token.isCancellationRequested) { + throw new CancellationError(); + } - // Rename - try { - this.logService.trace(`Started renaming the extension from ${tempLocation.fsPath} to ${extensionLocation.fsPath}`); - await this.rename(tempLocation.fsPath, extensionLocation.fsPath); - this.logService.info('Renamed to', extensionLocation.fsPath); - } 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; - } + // Rename + try { + this.logService.trace(`Started renaming the extension from ${tempLocation.fsPath} to ${extensionLocation.fsPath}`); + await this.rename(tempLocation.fsPath, extensionLocation.fsPath); + this.logService.info('Renamed to', extensionLocation.fsPath); + } 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; } + } - this._onExtract.fire(extensionLocation); + this._onExtract.fire(extensionLocation); - } catch (error) { - try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } - throw error; - } + } catch (error) { + try { await this.fileService.del(tempLocation, { recursive: true }); } catch (e) { /* ignore */ } + throw error; } return this.scanLocalExtension(extensionLocation, ExtensionType.User); @@ -642,6 +664,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); @@ -759,7 +782,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 }); @@ -912,13 +935,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; } @@ -945,16 +964,15 @@ 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 ba8e026b8935f..a56087b0b5127 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 ce93c6e73d732..178293d88742a 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 cebafab4714a7..a4c9afd9c5826 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 d0813771c1878..1c3d62f7f0b92 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 a3c2603a1eb36..64aad4fbd4119 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 5e71b85714e77..d94305a83fc55 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 e803de72c79df..bae93bde80352 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 9e72c1c174ab6..8c0569fe67b9a 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 cee5eaeedefc5..5fae8b4b1e2fa 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 cfe5313b905c1..822260bdc2fdf 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 0000000000000..0429fdd1510f4 --- /dev/null +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -0,0 +1,382 @@ +/*--------------------------------------------------------------------------------------------- + * 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', + }, + contribSourceControlHistoryItemChangesMenu: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemChangesMenu.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', + }, + testMessageStackTrace: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testMessageStackTrace.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 fec3b6eded6d6..106963ac0b514 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 { Promises } from 'vs/base/common/async'; 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 { 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 { Promises } from 'vs/base/common/async'; +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 9060783778ac2..6ac5821e08a54 100644 --- a/src/vs/platform/extensions/test/common/extensionValidator.test.ts +++ b/src/vs/platform/extensions/test/common/extensionValidator.test.ts @@ -2,10 +2,10 @@ * 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', () => { @@ -423,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 0000000000000..7b81268b3476a --- /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 149665bb57100..82d4f9ba0f3b6 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 05ff156de894c..eef16ccfcb0bb 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 180aa7e2960ff..9993c5f82ba91 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 3576f20d4f7bc..c5665396da9b4 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 c71f1581cf6b5..eec6a2232c99c 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 afabd7aed1271..46d213c12e8da 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 114e98adefc17..166cf54968898 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 9290520ecdae4..c5f542918bafb 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 1de7f86398af5..245d2222b9d4c 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 23776f54d82a4..6d7bb7f3b8322 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 fbf1ea0d870c5..c3c271abbdd8e 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 836b67ed9d222..c53f15426aba4 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 db85e28f34143..4370d82bf904a 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/test/browser/nullHoverService.ts b/src/vs/platform/hover/test/browser/nullHoverService.ts index 6b7b728325b6b..44c73f8a0a69d 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 4b313ed32ebd0..a61ef74333df5 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 c5abc22ccff82..99e8e0e75a0bf 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 fd5bb77d2f72a..70718ba712d9b 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 81f3a8083caad..856c58c1873be 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 62e2d9ff34108..8f7ac7dfcc396 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,7 +81,12 @@ 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( @@ -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 0000000000000..76da45d897cf9 --- /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/jsonschemas/common/jsonContributionRegistry.ts b/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts index 08322db2bb606..039aaa6cc9b53 100644 --- a/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts +++ b/src/vs/platform/jsonschemas/common/jsonContributionRegistry.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { getCompressedContent, IJSONSchema } from 'vs/base/common/jsonSchema'; import * as platform from 'vs/platform/registry/common/platform'; export const Extensions = { @@ -35,6 +35,18 @@ export interface IJSONContributionRegistry { * Get all schemas */ getSchemaContributions(): ISchemaContributions; + + /** + * Gets the (compressed) content of the schema with the given schema ID (if any) + * @param uri The id of the schema + */ + getSchemaContent(uri: string): string | undefined; + + /** + * Returns true if there's a schema that matches the given schema ID + * @param uri The id of the schema + */ + hasSchemaContent(uri: string): boolean; } @@ -74,6 +86,15 @@ class JSONContributionRegistry implements IJSONContributionRegistry { }; } + public getSchemaContent(uri: string): string | undefined { + const schema = this.schemasById[uri]; + return schema ? getCompressedContent(schema) : undefined; + } + + public hasSchemaContent(uri: string): boolean { + return !!this.schemasById[uri]; + } + } const jsonContributionRegistry = new JSONContributionRegistry(); diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 2f79c811d5e73..88aa86a05d3b3 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 dfc5df1e096b8..b88a747e454c0 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 54bd2a6761dad..3fccf38754050 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 3a7df771bc2cc..698b9b56de759 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 7dfc46d0a3b96..4013b2b548be5 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 6857531cb6681..976b9a85dc315 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/test/common/markerService.test.ts b/src/vs/platform/markers/test/common/markerService.test.ts index d8ebccf746fd2..1b9916a8adfd9 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 7e96549a723b7..d7449e9454153 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 c680afa296ab6..731070e4f50a0 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 4c035680fb7b1..8a45868ed6367 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; @@ -184,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 bf2a99f601c69..15918242bba79 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 bb5d30f083bd7..e3013c06f8ca5 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; } @@ -765,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 096993beb8018..2e886ef6540b2 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 0000000000000..cfecf902c5f7f --- /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 710292ef17de5..eb93b66a122c2 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 35b6e027d66f3..93ee50f9901c8 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 9d3f34e443d29..58278d978f9e3 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -53,7 +53,7 @@ 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) { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index e1eb0a2f5f296..2a1c7349aaeb8 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 85bce306781a1..24c2ddb78defb 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 4c6dfbe6b7d6d..74414a631a107 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -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 diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 626b74e8fa520..8999c5e84ab8a 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -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 6fa28c37170a3..83976c1506535 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() { @@ -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 4fd3faf30e0ad..12b7afa785e09 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 8e8fc694be0c9..328c43d72fb8e 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 3fe9188fa8d04..8ec965d503b34 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 792c0352bb781..a26de9d171ba6 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 3ba33f74b96c3..4bc71a35f0cd2 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 cfbc105f465f7..ed564551df9a8 100644 --- a/src/vs/platform/remote/test/common/remoteHosts.test.ts +++ b/src/vs/platform/remote/test/common/remoteHosts.test.ts @@ -3,7 +3,7 @@ * Licensed 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'; 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 a5bacbb8c86bb..67790a807c634 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 f0fc3d6879001..70f4e65bcbc95 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 289ad6740e0c8..e8574d11f7fba 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 421106ebff6d0..b489a52dacc2b 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/proxy.ts b/src/vs/platform/request/node/proxy.ts index db448e06cc1c9..141be5bb796e2 100644 --- a/src/vs/platform/request/node/proxy.ts +++ b/src/vs/platform/request/node/proxy.ts @@ -44,7 +44,21 @@ export async function getProxyAgent(rawRequestURL: string, env: typeof process.e rejectUnauthorized: isBoolean(options.strictSSL) ? options.strictSSL : true, }; - return requestURL.protocol === 'http:' - ? new (await import('http-proxy-agent')).HttpProxyAgent(proxyURL, opts) - : new (await import('https-proxy-agent')).HttpsProxyAgent(proxyURL, opts); + if (requestURL.protocol === 'http:') { + // ESM-comment-begin + const mod = await import('http-proxy-agent'); + // ESM-comment-end + // ESM-uncomment-begin + // const mod = (await import('http-proxy-agent')).default; + // ESM-uncomment-end + return new mod.HttpProxyAgent(proxyURL, opts); + } else { + // ESM-comment-begin + const mod = await import('https-proxy-agent'); + // ESM-comment-end + // ESM-uncomment-begin + // const mod = (await import('https-proxy-agent')).default; + // ESM-uncomment-end + return new mod.HttpsProxyAgent(proxyURL, opts); + } } diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 23f8f0d44c850..dd2165c1ce312 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 b3a048af2f25f..50def3a9a92a6 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 493d78d0e5179..4674f20a4ee37 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 c110f28015b8c..8d6d1b539d5e1 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 72f87dddf3506..d1770958d3b36 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 2ee6f9bc99b6c..33a22c391b292 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 8524e9a6996ec..bc75667e9496f 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 8de29c1eeeb33..2fe9b7a647778 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 426de4a8ff7ec..5cd3d9be9552b 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 0f1c53efded8f..dbce8f5445817 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 8df3d3b8f4282..bef121fbff585 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 79a828cc220a7..417527a976fc9 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 cacc170e5cd3f..d335e2c27cd95 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 8c74c72b9c91f..ebd0331692ee4 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 f91b87baed429..402dcc7b723f3 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/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index ec8182b3c1df1..f729563700350 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -15,7 +15,6 @@ import { RequestStore } from 'vs/platform/terminal/common/requestStore'; import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IShellLaunchConfig, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, IFixedTerminalDimensions, IPersistentTerminalProcessLaunchConfig, ICrossVersionSerializedTerminalState, ISerializedTerminalState, ITerminalProcessOptions, IPtyHostLatencyMeasurement } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; -import { Terminal as XtermTerminal } from '@xterm/headless'; import type { ISerializeOptions, SerializeAddon as XtermSerializeAddon } from '@xterm/addon-serialize'; import type { Unicode11Addon as XtermUnicode11Addon } from '@xterm/addon-unicode11'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto } from 'vs/platform/terminal/common/terminalProcess'; @@ -32,6 +31,14 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { join } from 'path'; import { memoize } from 'vs/base/common/decorators'; import * as performance from 'vs/base/common/performance'; +// ESM-comment-begin +import { Terminal as XtermTerminal } from '@xterm/headless'; +// ESM-comment-end +// ESM-uncomment-begin +// import pkg from '@xterm/headless'; +// type XtermTerminal = pkg.Terminal; +// const { Terminal: XtermTerminal } = pkg; +// ESM-uncomment-end export function traceRpc(_target: any, key: string, descriptor: any) { if (typeof descriptor.value !== 'function') { diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 813fbe5c43251..7349ac1e9db64 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 a799870b5484e..2679ea3683afb 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 e289fbc34fc06..97a85375c951f 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 4566eb4b00817..667442dd8c9b5 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 b1c523a2228e4..66b317c468b6e 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 2f68a7be2df98..dd4e198d7d45c 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 871178faf38c1..a0df50920abb2 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 2388e7cb7024c..14ceea8847b5c 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 9a02e218db869..baf6b86f27f16 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.")); @@ -65,7 +65,7 @@ export const textSeparatorForeground = registerColor('textSeparator.foreground', // ------ text preformat export const textPreformatForeground = registerColor('textPreformat.foreground', - { light: '#A31515', dark: '#D7BA7D', hcDark: '#FFFFFF', hcLight: '#000000' }, + { light: '#A31515', dark: '#D7BA7D', hcDark: '#000000', hcLight: '#FFFFFF' }, nls.localize('textPreformatForeground', "Foreground color for preformatted text segments.")); export const textPreformatBackground = registerColor('textPreformat.background', diff --git a/src/vs/platform/theme/common/colors/chartsColors.ts b/src/vs/platform/theme/common/colors/chartsColors.ts index eb63b6022347d..a35e296d2ad2c 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 a57b85e2c29ca..cac6dea162ccb 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', @@ -142,7 +142,7 @@ export const editorFindMatch = registerColor('editor.findMatchBackground', nls.localize('editorFindMatch', "Color of the current search match.")); export const editorFindMatchForeground = registerColor('editor.findMatchForeground', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('editorFindMatchForeground', "Text color of the current search match.")); export const editorFindMatchHighlight = registerColor('editor.findMatchHighlightBackground', @@ -150,7 +150,7 @@ export const editorFindMatchHighlight = registerColor('editor.findMatchHighlight 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', - { light: null, dark: null, hcDark: null, hcLight: null }, + null, nls.localize('findMatchHighlightForeground', "Foreground color of the other search matches."), true); export const editorFindRangeHighlight = registerColor('editor.findRangeHighlightBackground', @@ -177,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', @@ -204,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')); @@ -231,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.")); @@ -242,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', @@ -278,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.')); @@ -314,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', @@ -355,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', @@ -371,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.")); @@ -389,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', @@ -397,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', @@ -405,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', @@ -430,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 dc38222d402d0..c79c1d2840b8e 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 b6f51e3696b07..dd5c405199c80 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 6fa9a0ec3267a..05bf549195248 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 0b051994d09d3..ade38578c28ad 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 5a2ea49b7026b..42a00e23e6ae8 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 7f8fc271a6e0c..3b109a21872ae 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 caef715f2daec..e332feed26436 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 86b4da4b409af..b1433f2e74588 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 d86d3f47bd753..ae32707eb3051 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 27d69e4e5aced..32d33a7d23c5b 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 4c49a758185db..a2561be0c114d 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 32dde9e11d704..339e86bf938de 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 b6a05ce558fc2..8f0c5641d9f37 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 b65c078f83a12..f18ae0970505e 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.transientProfilesObject.emptyWindows.values()].some(windowProfile => this.uriIdentityService.extUri.isEqual(windowProfile.location, profile.location))) { + if ([...this.profilesObject.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))) { + if ([...this.profilesObject.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))) { + if ([...this.transientProfilesObject.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.workspaces.values()].some(workspaceProfile => this.uriIdentityService.extUri.isEqual(workspaceProfile.location, profile.location))) { + return true; + } + 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) { - // Unset the transiet workspace association if any - this.updateWorkspaceAssociation(workspaceIdentifier, undefined, true); - } + if (transient) { + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) { + this.transientProfilesObject.folders.delete(workspaceIdentifier.uri); + if (newProfile) { + this.transientProfilesObject.folders.set(workspaceIdentifier.uri, newProfile); + } + } - const workspace = this.getWorkspace(workspaceIdentifier); - const profilesObject = transient ? this.transientProfilesObject : this.profilesObject; + else if (isWorkspaceIdentifier(workspaceIdentifier)) { + this.transientProfilesObject.workspaces.delete(workspaceIdentifier.configPath); + if (newProfile) { + this.transientProfilesObject.workspaces.set(workspaceIdentifier.configPath, newProfile); + } + } - // Folder or Multiroot workspace - if (URI.isUri(workspace)) { - profilesObject.workspaces.delete(workspace); - if (newProfile) { - profilesObject.workspaces.set(workspace, newProfile); + else { + this.transientProfilesObject.emptyWindows.delete(workspaceIdentifier.id); + if (newProfile) { + this.transientProfilesObject.emptyWindows.set(workspaceIdentifier.id, newProfile); + } } } - // Empty Window + else { - profilesObject.emptyWindows.delete(workspace); - if (newProfile) { - profilesObject.emptyWindows.set(workspace, newProfile); - } - } + // Unset the transiet workspace association if any + this.updateWorkspaceAssociation(workspaceIdentifier, undefined, true); + const workspace = this.getWorkspace(workspaceIdentifier); - if (!transient) { + // Folder or Multiroot workspace + if (URI.isUri(workspace)) { + this.profilesObject.workspaces.delete(workspace); + if (newProfile) { + this.profilesObject.workspaces.set(workspace, newProfile); + } + } + // Empty Window + else { + this.profilesObject.emptyWindows.delete(workspace); + if (newProfile) { + this.profilesObject.emptyWindows.set(workspace, newProfile); + } + } this.updateStoredProfileAssociations(); } } diff --git a/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts index a04c44f96ef50..a9a7b3771c7a5 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 0679efcc74208..313bedc9bffc5 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 3b37d056aae10..703011c9d607e 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 7e898dd15fa45..a7e2da87458b5 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 48a68ee98a186..36cfb4efce5a3 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 515b2ce80da2b..8797fafc6c4b7 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 53561e249db7e..7af2df9134a8d 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 b1c7523e8efa4..c9499d33e9ef5 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 084755420cb32..ed81c9196e895 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 28d2400cfee3c..8c505b3aec7ba 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 856003580d014..a11d2cb04cc7e 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 3436a31fb0782..1ecc7b900a419 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 3c61cd98aca10..7a17fac286d42 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 d27a2d8d6ac1b..7d3ecaca73268 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 935c553a8727e..c7a1f8675e7fe 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 4799201077a13..660185fd8fda5 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 274ac5ee6962c..625df21215c5a 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 0cd110208b813..39246f25ce14f 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 f41039cbb9ecc..50e5caa545f04 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 fc9644c22450f..97f4ab83f13f2 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 1e854038a9d01..a336ca3241166 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 73fe0a6b23d52..c6cfd18a44bd4 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 47cde8003c7af..e9c86afd301bd 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 40cc6f3623dcf..e60f53147992f 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 2d6d7de2bf0bc..2227e3d11826c 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 b622a3efe44b1..3cd97bda58505 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 665d09ed41eae..a06d711277e60 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 db83d62e16330..03f68a73da9b0 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/window/common/window.ts b/src/vs/platform/window/common/window.ts index 16eef424015ad..2b7ffc4651f01 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 { diff --git a/src/vs/platform/window/electron-main/window.ts b/src/vs/platform/window/electron-main/window.ts index b63a6117514de..15fcb1790fe0a 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 dcb3a1776ecfa..0f626a7ddfc90 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 abfc98bc2a31a..30ed4fe16b0ff 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 8c9cb2b186853..070c7ae05d384 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()) { @@ -1444,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 df866ea355612..6a6591cd44d22 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 490d6858a62dc..f3ce84f9c7ac5 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 0b96b1bf740da..65d1a9937f980 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 2464d5e4a3576..fb8c1cf18e65d 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 236f6d6fc323e..bfb4fcd51459a 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 a1f2f262d0075..a08f1035ce139 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 56e4d92fdc983..553be4fa37cc0 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 2677ffba87dcc..f2102c3b0982e 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 0234ce57bb9a3..92f1ac9f58903 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 f259ea2cbafc5..f345bd69a2db9 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 { 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 { createRandomIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; +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 { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; -import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; -import { removeDangerousEnvVariables } from 'vs/base/common/processes'; +import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +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 5430ae1916281..78dc3bce4f007 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 9eace08d06a2b..29aa95e5683fe 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 19a50f7b067ff..7b58140ea1b6f 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); @@ -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 682b6f2b088ad..2a1ea9f5699be 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; - }); -} + // NLS: below 2 are a relic from old times only used by vscode-nls and deprecated + locale: 'en', + availableLanguages: {} + }; + } -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'; + 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/server.cli.ts b/src/vs/server/node/server.cli.ts index 2eed66f7ee06d..fc0f7c259e017 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as _fs from 'fs'; -import * as _url from 'url'; -import * as _cp from 'child_process'; -import * as _http from 'http'; -import * as _os from 'os'; +import * as fs from 'fs'; +import * as url from 'url'; +import * as cp from 'child_process'; +import * as http from 'http'; import { cwd } from 'vs/base/common/process'; import { dirname, extname, resolve, join } from 'vs/base/common/path'; import { parseArgs, buildHelpMessage, buildVersionMessage, OPTIONS, OptionDescriptions, ErrorReporter } from 'vs/platform/environment/node/argv'; @@ -240,8 +239,8 @@ export async function main(desc: ProductDescription, args: string[]): Promise console.log(err)); + const childProcess = cp.fork(join(__dirname, '../../../server-main.js'), cmdLine, { stdio: 'inherit' }); + childProcess.on('error', err => console.log(err)); return; } @@ -270,7 +269,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise process.stdout.write(data)); - cp.stderr.on('data', data => process.stderr.write(data)); + const childProcess = cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: ['inherit', 'pipe', 'pipe'] }); + childProcess.stdout.on('data', data => process.stdout.write(data)); + childProcess.stderr.on('data', data => process.stderr.write(data)); } else { - _cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: 'inherit' }); + cp.spawn(cliCommand, newCommandline, { cwd: cliCwd, env, stdio: 'inherit' }); } } } else { @@ -357,7 +356,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise setTimeout(res, 1000)); } } @@ -376,7 +375,7 @@ function openInBrowser(args: string[], verbose: boolean) { for (const location of args) { try { if (/^(http|https|file):\/\//.test(location)) { - uris.push(_url.parse(location).href); + uris.push(url.parse(location).href); } else { uris.push(pathToURI(location).href); } @@ -406,7 +405,7 @@ function sendToPipe(args: PipeCommand, verbose: boolean): Promise { return; } - const opts: _http.RequestOptions = { + const opts: http.RequestOptions = { socketPath: cliPipe, path: '/', method: 'POST', @@ -416,7 +415,7 @@ function sendToPipe(args: PipeCommand, verbose: boolean): Promise { } }; - const req = _http.request(opts, res => { + const req = http.request(opts, res => { if (res.headers['content-type'] !== 'application/json') { reject('Error in response: Invalid content type: Expected \'application/json\', is: ' + res.headers['content-type']); return; @@ -461,18 +460,18 @@ function fatal(message: string, err: any): void { const preferredCwd = process.env.PWD || cwd(); // prefer process.env.PWD as it does not follow symlinks -function pathToURI(input: string): _url.URL { +function pathToURI(input: string): url.URL { input = input.trim(); input = resolve(preferredCwd, input); - return _url.pathToFileURL(input); + return url.pathToFileURL(input); } function translatePath(input: string, mapFileUri: (input: string) => string, folderURIS: string[], fileURIS: string[]) { const url = pathToURI(input); const mappedUri = mapFileUri(url.href); try { - const stat = _fs.lstatSync(_fs.realpathSync(input)); + const stat = fs.lstatSync(fs.realpathSync(input)); if (stat.isFile()) { fileURIS.push(mappedUri); diff --git a/src/vs/server/node/serverConnectionToken.ts b/src/vs/server/node/serverConnectionToken.ts index f976d0e379a45..6a6bdcdb56a57 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 6dc550b6cf010..a8ac6043a511f 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 b624dd6d67660..b04dab4d30852 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 9fdc4b3030618..1d563ea1dcefd 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 3719825841a77..d833eab2860c2 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 @@ -173,21 +174,21 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu throw new Error('Invalid combination of options. Please remove one of the following: createIfNone, silent'); } + if (options.clearSessionPreference) { + // Clearing the session preference is usually paired with createIfNone, so just remove the preference and + // defer to the rest of the logic in this function to choose the session. + this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); + } + // Check if the sessions we have are valid if (!options.forceNewSession && sessions.length) { if (provider.supportsMultipleAccounts) { - if (options.clearSessionPreference) { - // Clearing the session preference is usually paired with createIfNone, so just remove the preference and - // defer to the rest of the logic in this function to choose the session. - this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); - } else { - // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. - const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); - if (existingSessionPreference) { - const matchingSession = sessions.find(session => session.id === existingSessionPreference); - if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { - return matchingSession; - } + // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. + const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); + if (existingSessionPreference) { + const matchingSession = sessions.find(session => session.id === existingSessionPreference); + if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { + return matchingSession; } } } else if (this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { @@ -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 0d63847dae5ce..35126b7998d27 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 bf7103206a0d9..9e08e5d14232c 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 4eb6385cbed35..a0f00ce4416a6 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; @@ -181,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 @@ -188,6 +192,8 @@ export class MainThreadCommentThread implements languages.CommentThread { this._isDisposed = false; if (_isTemplate) { this.comments = []; + } else if (comments) { + this._comments = comments; } } @@ -294,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 { @@ -304,6 +311,7 @@ export class MainThreadCommentController implements ICommentController { threadId, URI.revive(resource).toString(), range, + comments, true, isTemplate, editorId @@ -520,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); @@ -584,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 @@ -594,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, @@ -631,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 f58ba4c47fb99..36b5a4df0aee4 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 48888b0ddcb73..9bf27279db0dd 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 2d05a6a0e4421..e159117994487 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 6732d38f5e670..7c4db0a5def96 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 1781e73af7ac9..4aa61aeab4591 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -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 0000000000000..fbda6ced5a3e4 --- /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 9b14928a5148c..adea665487c16 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 c036901f90760..b50fee3527cec 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 a54fc865a9839..af9d3f401cfb9 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'; @@ -25,7 +27,6 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { Schemas } from 'vs/base/common/network'; import { ITextModel } from 'vs/editor/common/model'; -import { observableValue } from 'vs/base/common/observable'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -40,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, @@ -163,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> { @@ -171,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 { @@ -189,6 +205,9 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { })); } + $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { + this._currentHistoryItemGroupObs.set(historyItemGroup, undefined); + } } class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { @@ -228,8 +247,12 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { 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; } @@ -237,9 +260,6 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _commitTemplate = observableValue(this, ''); get commitTemplate() { return this._commitTemplate; } - private readonly _onDidChangeStatusBarCommands = new Emitter(); - get onDidChangeStatusBarCommands(): Event { return this._onDidChangeStatusBarCommands.event; } - 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, @@ -280,8 +302,12 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { 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 a3641b6687aab..1b2a115fca23f 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 180932b6d6d26..09ff17ad867c8 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 496651fe769c7..8b7c408c76628 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 680d04cb5ddaa..7847141b2f167 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 @@ -1677,6 +1686,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TestResultState: extHostTypes.TestResultState, TestRunRequest: extHostTypes.TestRunRequest, TestMessage: extHostTypes.TestMessage, + TestMessage2: extHostTypes.TestMessage, + TestMessageStackFrame: extHostTypes.TestMessageStackFrame, TestTag: extHostTypes.TestTag, TestRunProfileKind: extHostTypes.TestRunProfileKind, TextSearchCompleteMessageType: TextSearchCompleteMessageType, @@ -1731,12 +1742,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 d01a3219f948a..0427ebe7b17b4 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 2d10d6b712967..7bbab68a618c7 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 16b1d4a405ba1..c5ba402ef072f 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 9e8bd3e587c19..e029c680e427c 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'; @@ -210,11 +212,11 @@ class ChatAgentResponseStream { _report(dto); return this; }, - confirmation(title, message, data) { + confirmation(title, message, data, buttons) { throwIfDone(this.confirmation); checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); - const part = new extHostTypes.ChatResponseConfirmationPart(title, message, data); + const part = new extHostTypes.ChatResponseConfirmationPart(title, message, data, buttons); const dto = typeConvert.ChatResponseConfirmationPart.from(part); _report(dto); return this; @@ -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 @@ -368,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)); } @@ -379,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); @@ -433,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)); } @@ -456,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> { @@ -498,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, @@ -521,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 []; @@ -583,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 5f0bf7d244953..dfc37201bd40f 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 b3f54666152a6..c84fb4782f9eb 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 d5ea2bdf817fa..a5e22aa3dee1a 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 e23ce395cc8f7..da9a087fbb10c 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 c33cb0704dd91..372037aa3419f 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 ee321b2575a87..b3edbad94a168 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 f99cd387c62c1..4c712aee9e8f4 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 4ea250c3bf8d6..97935e89d0893 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 215ff5fda3722..0706a87cedb2b 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 0000000000000..e588f50ab6da2 --- /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 fdc937315540f..b67999350aff4 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,13 +99,17 @@ class LanguageModelResponse { } this._responseStreams.set(fragment.index, res); } - res.stream.emitOne(fragment.part); - } - get isStreaming(): boolean { - return this._isStreaming; + 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); } + reject(err: Error): void { this._isDone = true; for (const stream of this._streams()) { @@ -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 @@ -311,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); - - const barrier = new Barrier(); - - const res = new LanguageModelResponse(); - this._pendingRequest.set(requestId, { languageModelId, res }); + try { + const requestId = (Math.random() * 1e6) | 0; + const res = new LanguageModelResponse(); + this._pendingRequest.set(requestId, { languageModelId, res }); - let error: Error | undefined; + try { + await this._proxy.$tryStartChatRequest(from, languageModelId, requestId, internalMessages, options, token); - 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; + } 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; } - }).finally(() => { - this._pendingRequest.delete(requestId); - res.resolve(); - barrier.open(); - }); - await barrier.wait(); + return res.apiObject; - if (error) { + } 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[]) { @@ -358,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 diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 3a7e105c8437f..5b707fc865eb0 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 8f74a0a4b6937..382fd39c30a4e 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 8472fc1006dbb..4aec54c265163 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 c63a3afd1722b..46f25cb7dd276 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -9,7 +9,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { debounce } from 'vs/base/common/decorators'; import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { asPromise, Sequencer } from 'vs/base/common/async'; +import { asPromise } from 'vs/base/common/async'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol'; import { sortedDiff, equals } from 'vs/base/common/arrays'; @@ -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; @@ -259,7 +266,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { set value(value: string) { value = value ?? ''; - this._sequencer.queue(async () => this.#proxy.$setInputBoxValue(this._sourceControlHandle, value)); + this.#proxy.$setInputBoxValue(this._sourceControlHandle, value); this.updateValue(value); } @@ -276,7 +283,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { } set placeholder(placeholder: string) { - this._sequencer.queue(async () => this.#proxy.$setInputBoxPlaceholder(this._sourceControlHandle, placeholder)); + this.#proxy.$setInputBoxPlaceholder(this._sourceControlHandle, placeholder); this._placeholder = placeholder; } @@ -296,7 +303,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { } this._validateInput = fn; - this._sequencer.queue(async () => this.#proxy.$setValidationProviderIsEnabled(this._sourceControlHandle, !!fn)); + this.#proxy.$setValidationProviderIsEnabled(this._sourceControlHandle, !!fn); } private _enabled: boolean = true; @@ -313,7 +320,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { } this._enabled = enabled; - this._sequencer.queue(async () => this.#proxy.$setInputBoxEnablement(this._sourceControlHandle, enabled)); + this.#proxy.$setInputBoxEnablement(this._sourceControlHandle, enabled); } private _visible: boolean = true; @@ -330,7 +337,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { } this._visible = visible; - this._sequencer.queue(async () => this.#proxy.$setInputBoxVisibility(this._sourceControlHandle, visible)); + this.#proxy.$setInputBoxVisibility(this._sourceControlHandle, visible); } get document(): vscode.TextDocument { @@ -339,7 +346,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { return this.#extHostDocuments.getDocument(this._documentUri); } - constructor(private _extension: IExtensionDescription, _extHostDocuments: ExtHostDocuments, proxy: MainThreadSCMShape, private _sequencer: Sequencer, private _sourceControlHandle: number, private _documentUri: URI) { + constructor(private _extension: IExtensionDescription, _extHostDocuments: ExtHostDocuments, proxy: MainThreadSCMShape, private _sourceControlHandle: number, private _documentUri: URI) { this.#extHostDocuments = _extHostDocuments; this.#proxy = proxy; } @@ -347,7 +354,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox { showValidationMessage(message: string | vscode.MarkdownString, type: vscode.SourceControlInputBoxValidationType) { checkProposedApiEnabled(this._extension, 'scmValidation'); - this._sequencer.queue(async () => this.#proxy.$showValidationMessage(this._sourceControlHandle, message, type as any)); + this.#proxy.$showValidationMessage(this._sourceControlHandle, message, type as any); } $onInputBoxValueChange(value: string): void { @@ -386,14 +393,14 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG get label(): string { return this._label; } set label(label: string) { this._label = label; - this._sequencer.queue(async () => this._proxy.$updateGroupLabel(this._sourceControlHandle, this.handle, label)); + this._proxy.$updateGroupLabel(this._sourceControlHandle, this.handle, label); } private _hideWhenEmpty: boolean | undefined = undefined; get hideWhenEmpty(): boolean | undefined { return this._hideWhenEmpty; } set hideWhenEmpty(hideWhenEmpty: boolean | undefined) { this._hideWhenEmpty = hideWhenEmpty; - this._sequencer.queue(async () => this._proxy.$updateGroup(this._sourceControlHandle, this.handle, this.features)); + this._proxy.$updateGroup(this._sourceControlHandle, this.handle, this.features); } get features(): SCMGroupFeatures { @@ -413,7 +420,6 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG constructor( private _proxy: MainThreadSCMShape, private _commands: ExtHostCommands, - private _sequencer: Sequencer, private _sourceControlHandle: number, private _id: string, private _label: string, @@ -512,7 +518,6 @@ class ExtHostSourceControl implements vscode.SourceControl { #proxy: MainThreadSCMShape; - private readonly _sequencer = new Sequencer(); private _groups: Map = new Map(); get id(): string { @@ -542,7 +547,7 @@ class ExtHostSourceControl implements vscode.SourceControl { } this._count = count; - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { count })); + this.#proxy.$updateSourceControl(this.handle, { count }); } private _quickDiffProvider: vscode.QuickDiffProvider | undefined = undefined; @@ -557,7 +562,7 @@ class ExtHostSourceControl implements vscode.SourceControl { if (isProposedApiEnabled(this._extension, 'quickDiffProvider')) { quickDiffLabel = quickDiffProvider?.label; } - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel })); + this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel }); } private _historyProvider: vscode.SourceControlHistoryProvider | undefined; @@ -575,12 +580,12 @@ class ExtHostSourceControl implements vscode.SourceControl { this._historyProvider = historyProvider; this._historyProviderDisposable.value = new DisposableStore(); - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { hasHistoryProvider: !!historyProvider })); + this.#proxy.$updateSourceControl(this.handle, { hasHistoryProvider: !!historyProvider }); if (historyProvider) { this._historyProviderDisposable.value.add(historyProvider.onDidChangeCurrentHistoryItemGroup(() => { this._historyProviderCurrentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - this._sequencer.queue(async () => this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup)); + this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup); })); } } @@ -597,7 +602,7 @@ class ExtHostSourceControl implements vscode.SourceControl { } this._commitTemplate = commitTemplate; - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { commitTemplate })); + this.#proxy.$updateSourceControl(this.handle, { commitTemplate }); } private readonly _acceptInputDisposables = new MutableDisposable(); @@ -613,7 +618,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this._acceptInputCommand = acceptInputCommand; const internal = this._commands.converter.toInternal(acceptInputCommand, this._acceptInputDisposables.value); - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { acceptInputCommand: internal })); + this.#proxy.$updateSourceControl(this.handle, { acceptInputCommand: internal }); } private readonly _actionButtonDisposables = new MutableDisposable(); @@ -637,7 +642,7 @@ class ExtHostSourceControl implements vscode.SourceControl { description: actionButton.description, enabled: actionButton.enabled } : undefined; - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { actionButton: internal ?? null })); + this.#proxy.$updateSourceControl(this.handle, { actionButton: internal ?? null }); } @@ -658,7 +663,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this._statusBarCommands = statusBarCommands; const internal = (statusBarCommands || []).map(c => this._commands.converter.toInternal(c, this._statusBarDisposables.value!)) as ICommandDto[]; - this._sequencer.queue(async () => this.#proxy.$updateSourceControl(this.handle, { statusBarCommands: internal })); + this.#proxy.$updateSourceControl(this.handle, { statusBarCommands: internal }); } private _selected: boolean = false; @@ -689,8 +694,8 @@ class ExtHostSourceControl implements vscode.SourceControl { query: _rootUri ? `rootUri=${encodeURIComponent(_rootUri.toString())}` : undefined }); - this._sequencer.queue(() => this.#proxy.$registerSourceControl(this.handle, _id, _label, _rootUri, inputBoxDocumentUri)); - this._inputBox = new ExtHostSCMInputBox(_extension, _extHostDocuments, this.#proxy, this._sequencer, this.handle, inputBoxDocumentUri); + this._inputBox = new ExtHostSCMInputBox(_extension, _extHostDocuments, this.#proxy, this.handle, inputBoxDocumentUri); + this.#proxy.$registerSourceControl(this.handle, _id, _label, _rootUri, inputBoxDocumentUri); } private createdResourceGroups = new Map(); @@ -698,7 +703,7 @@ class ExtHostSourceControl implements vscode.SourceControl { createResourceGroup(id: string, label: string, options?: { multiDiffEditorEnableViewChanges?: boolean }): ExtHostSourceControlResourceGroup { const multiDiffEditorEnableViewChanges = isProposedApiEnabled(this._extension, 'scmMultiDiffEditor') && options?.multiDiffEditorEnableViewChanges === true; - const group = new ExtHostSourceControlResourceGroup(this.#proxy, this._commands, this._sequencer, this.handle, id, label, multiDiffEditorEnableViewChanges, this._extension); + const group = new ExtHostSourceControlResourceGroup(this.#proxy, this._commands, this.handle, id, label, multiDiffEditorEnableViewChanges, this._extension); const disposable = Event.once(group.onDidDispose)(() => this.createdResourceGroups.delete(group)); this.createdResourceGroups.set(group, disposable); this.eventuallyAddResourceGroups(); @@ -722,7 +727,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this.updatedResourceGroups.delete(group); updateListener.dispose(); this._groups.delete(group.handle); - this._sequencer.queue(async () => this.#proxy.$unregisterGroup(this.handle, group.handle)); + this.#proxy.$unregisterGroup(this.handle, group.handle); }); groups.push([group.handle, group.id, group.label, group.features, group.multiDiffEditorEnableViewChanges]); @@ -736,7 +741,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this._groups.set(group.handle, group); } - this._sequencer.queue(async () => this.#proxy.$registerGroups(this.handle, groups, splices)); + this.#proxy.$registerGroups(this.handle, groups, splices); this.createdResourceGroups.clear(); } @@ -755,7 +760,7 @@ class ExtHostSourceControl implements vscode.SourceControl { }); if (splices.length > 0) { - this._sequencer.queue(async () => this.#proxy.$spliceResourceStates(this.handle, splices)); + this.#proxy.$spliceResourceStates(this.handle, splices); } this.updatedResourceGroups.clear(); @@ -776,7 +781,7 @@ class ExtHostSourceControl implements vscode.SourceControl { this._statusBarDisposables.dispose(); this._groups.forEach(group => group.dispose()); - this._sequencer.queue(async () => this.#proxy.$unregisterSourceControl(this.handle)); + this.#proxy.$unregisterSourceControl(this.handle); } } @@ -971,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 { @@ -981,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/extHostSecrets.ts b/src/vs/workbench/api/common/extHostSecrets.ts index d1af02ed1a233..13fb3293a3576 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 b870c3ab033e5..9ce31e7147c91 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 48aaa9c491f4a..1ef9c81d439c8 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -15,11 +15,12 @@ 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'; @@ -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. */ @@ -606,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, @@ -617,24 +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'); - 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 @@ -682,7 +729,6 @@ class TestRunTracker extends Disposable { } ended = true; - testItemCoverageId.clear(); this.proxy.$finishedTestRunTask(runId, taskId); if (!--this.running) { this.markEnded(); @@ -766,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) || []; } /** @@ -790,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); } @@ -813,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); @@ -842,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); diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index 44ed8f3804fa5..73334e5ac8920 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 7ab8d65df5344..277422f9acba9 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 25a2134440bf5..8bf96750570ce 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 { @@ -1887,6 +1888,11 @@ export namespace TestMessage { actual: message.actualOutput, contextValue: message.contextValue, location: message.location && ({ range: Range.from(message.location.range), uri: message.location.uri }), + stackTrace: (message as vscode.TestMessage2).stackTrace?.map(s => ({ + label: s.label, + position: s.position && Position.from(s.position), + uri: s.file && URI.revive(s.file).toJSON(), + })), }; } @@ -2065,8 +2071,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, }; } } @@ -2243,23 +2249,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 + }; } } @@ -2307,7 +2359,8 @@ export namespace ChatResponseConfirmationPart { kind: 'confirmation', title: part.title, message: part.message, - data: part.data + data: part.data, + buttons: part.buttons }; } } @@ -2510,6 +2563,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 { @@ -2545,7 +2600,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, @@ -2554,7 +2609,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 }; } } @@ -2697,3 +2753,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 189c90b8f633e..772070502e63c 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) => { @@ -4038,9 +4069,11 @@ export class TestMessage implements vscode.TestMessage { public expectedOutput?: string; public actualOutput?: string; public location?: vscode.Location; - /** proposed: */ public contextValue?: string; + /** proposed: */ + public stackTrace?: TestMessageStackFrame[]; + public static diff(message: string | vscode.MarkdownString, expected: string, actual: string) { const msg = new TestMessage(message); msg.expectedOutput = expected; @@ -4056,6 +4089,19 @@ export class TestTag implements vscode.TestTag { constructor(public readonly id: string) { } } +export class TestMessageStackFrame { + /** + * @param label The name of the stack frame + * @param file The file URI of the stack frame + * @param position The position of the stack frame within the file + */ + constructor( + public label: string, + public file?: vscode.Uri, + public position?: Position, + ) { } +} + //#endregion //#region Test Coverage @@ -4119,7 +4165,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[] = [], ) { } } @@ -4346,10 +4392,13 @@ export class ChatResponseConfirmationPart { title: string; message: string; data: any; - constructor(title: string, message: string, data: any) { + buttons?: string[]; + + constructor(title: string, message: string, data: any, buttons?: string[]) { this.title = title; this.message = message; this.data = data; + this.buttons = buttons; } } @@ -4449,16 +4498,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 +4545,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 08b842e84fb53..61c5b48357c17 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 2bd275cbc21c4..50c47ba98a852 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 fd03fd9ea327d..dc48354e3728f 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/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 5ac0ddc7ec954..ec6bb2bd1f427 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -18,6 +18,10 @@ import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { realpathSync } from 'vs/base/node/extpath'; import { ExtHostConsoleForwarder } from 'vs/workbench/api/node/extHostConsoleForwarder'; import { ExtHostDiskFileSystemProvider } from 'vs/workbench/api/node/extHostDiskFileSystemProvider'; +// ESM-uncomment-begin +// import { createRequire } from 'node:module'; +// const require = createRequire(import.meta.url); +// ESM-uncomment-end class NodeModuleRequireInterceptor extends RequireInterceptor { @@ -109,7 +113,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { if (extensionId) { performance.mark(`code/extHost/willLoadExtensionCode/${extensionId}`); } - r = require.__$__nodeRequire(module.fsPath); + r = require.__$__nodeRequire(module.fsPath); } finally { if (extensionId) { performance.mark(`code/extHost/didLoadExtensionCode/${extensionId}`); diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 99f5d7614e8f7..d10dbbb7d99d8 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 8348d9490b292..e259b3b0d6dad 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 1349880610411..54b831094897d 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 785db7edd432e..80a60c1d4c120 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 * as performance from 'vs/base/common/performance'; +import { IURITransformer } from 'vs/base/common/uriIpc'; +import { realpath } from 'vs/base/node/extpath'; +import { Promises } from 'vs/base/node/pfs'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { PersistentProtocol, ProtocolConstants, BufferedEmitter } from 'vs/base/parts/ipc/common/ipc.net'; +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 { 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 { 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 { boolean } from 'vs/editor/common/config/editorOptions'; 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 519924eec138c..4840c8b5b8254 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// ESM-comment-begin import * as http from 'http'; import * as https from 'https'; import * as tls from 'tls'; import * as net from 'net'; +// ESM-comment-end import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration'; @@ -17,6 +19,16 @@ 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'; + +// ESM-uncomment-begin +// import { createRequire } from 'node:module'; +// const require = createRequire(import.meta.url); +// const http = require('http'); +// const https = require('https'); +// const tls = require('tls'); +// const net = require('net'); +// ESM-uncomment-end const systemCertificatesV2Default = false; @@ -32,9 +44,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 +80,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 +95,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 +152,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 +186,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 35e7ff353606e..d5db3ca4683f9 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 f5dfeeb258c47..da5e8bb9e6f04 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'; diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts index dd77886bbf05b..de02ffff74523 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 045f3d77cc136..be11d3a7353ba 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 5697ffe78cfe3..5353ca9c069e9 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 ef43b937a9883..298299b3e56b3 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 26b419f6e064e..8dca84bc5c1aa 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 6c6de8c7e170d..2c866daee54f8 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 2132482c3aa49..f4795adebb491 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 3f60f5ef89885..298a2811954c7 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 632487d43c0a3..9f0a28ec02ad5 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 3f4255c49c8dd..eda97538749f5 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 cedfaa5e426fb..75f2ccce2d7f5 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 ba5e260f4d76a..953aff7ea0c58 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 585e3745c107c..6b7de3ca7167c 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 465663c8288a4..a88e5a79bb92c 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 5a7ed7e4e70bc..49a2f011645cc 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 5a7e6f434c28b..a1341bad58c69 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 97bfb308b9f41..54878ddb0cd3e 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 1d4da28f1eadf..4a49c8031a217 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'; @@ -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) { @@ -829,7 +829,8 @@ suite('ExtHost Testing', () => { expected: undefined, contextValue: undefined, actual: undefined, - location: convert.location.from(message1.location) + location: convert.location.from(message1.location), + stackTrace: undefined, }] ]); @@ -846,6 +847,7 @@ suite('ExtHost Testing', () => { expected: undefined, actual: undefined, location: convert.location.from({ uri: test2.uri!, range: test2.range }), + stackTrace: undefined, }] ]); diff --git a/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts b/src/vs/workbench/api/test/browser/extHostTextEditor.test.ts index 106a30df4f8b0..98bc776615106 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 3f780a0329903..1e442c1ed88fc 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 ab38b202b6e45..40a148042ff44 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 6cef861c9c8f9..3d1ff69b2324b 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 b741292bd00cb..e87300aeb0541 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 6abca46d3ff11..5b9dd312c74d9 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 959705f233cb7..c6ef6939bfedf 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 0c5d47544d105..d7503ca0d507f 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 a3a8e47755ca1..0c5a02f660647 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 bf67789ff1011..2f89e98e3586b 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 8eaa58798d793..949c2a78ffa72 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 8331e51cc0381..fee65f2111975 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 d7190f1fe5c50..f0d0538ce226f 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 9809814a6c032..1ca165c1b5fcb 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 8a714d8130dd9..dd20e3cd70cff 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 57b58ab67f6aa..f4ead1836b2f7 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 5234d7abb3427..c784f4189507b 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 3a56656608b6c..bfd874acf2e60 100644 --- a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts +++ b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.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 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 { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +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'; @@ -85,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]); @@ -103,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]); @@ -274,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 928c06a1b2c76..1608511b5274f 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 1502ef3f565ae..af20a5e455b75 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 2f567ef7f4b33..57fe15e52b43f 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 1e166016ed363..dbe7355c8c228 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 2c20a3572e203..7823fa988c073 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 db6890b901710..188a94e1f51ad 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/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index c22caa967b32d..cffce2ce2675d 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 4cc439c1fb786..6ad52a2695eab 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -12,7 +12,7 @@ import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from 'vs/base/common/pl import { EditorInputCapabilities, GroupIdentifier, isResourceEditorInput, IUntypedEditorInput, pathsToEditors } from 'vs/workbench/common/editor'; 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 { Position, Parts, PanelOpensMaximizedOptions, IWorkbenchLayoutService, positionFromString, positionToString, panelOpensMaximizedFromString, PanelAlignment, ActivityBarPosition, LayoutSettings, MULTI_WINDOW_PARTS, SINGLE_WINDOW_PARTS, ZenModeSettings, EditorTabsMode, EditorActionsLocation, shouldShowCustomTitleBar, isHorizontal } from 'vs/workbench/services/layout/browser/layoutService'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -608,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); } @@ -630,7 +630,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } this.doUpdateLayoutConfiguration(); - }); + })); // Layout Initialization State const initialEditorsState = this.getInitialEditorsState(); @@ -676,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; @@ -1262,18 +1266,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const containerDimension = this.getContainerDimension(container); if (container === this.mainContainer) { - const panelPosition = this.getPanelPosition(); - const isColumn = panelPosition === Position.RIGHT || panelPosition === Position.LEFT; + const isPanelHorizontal = isHorizontal(this.getPanelPosition()); const takenWidth = (this.isVisible(Parts.ACTIVITYBAR_PART) ? this.activityBarPartView.minimumWidth : 0) + (this.isVisible(Parts.SIDEBAR_PART) ? this.sideBarPartView.minimumWidth : 0) + - (this.isVisible(Parts.PANEL_PART) && isColumn ? this.panelPartView.minimumWidth : 0) + + (this.isVisible(Parts.PANEL_PART) && !isPanelHorizontal ? this.panelPartView.minimumWidth : 0) + (this.isVisible(Parts.AUXILIARYBAR_PART) ? this.auxiliaryBarPartView.minimumWidth : 0); const takenHeight = (this.isVisible(Parts.TITLEBAR_PART, targetWindow) ? this.titleBarPartView.minimumHeight : 0) + (this.isVisible(Parts.STATUSBAR_PART, targetWindow) ? this.statusBarPartView.minimumHeight : 0) + - (this.isVisible(Parts.PANEL_PART) && !isColumn ? this.panelPartView.minimumHeight : 0); + (this.isVisible(Parts.PANEL_PART) && isPanelHorizontal ? this.panelPartView.minimumHeight : 0); const availableWidth = containerDimension.width - takenWidth; const availableHeight = containerDimension.height - takenHeight; @@ -1542,7 +1545,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // 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); + : isHorizontal(this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION)) + ? this.workbenchGrid.getViewSize(this.panelPartView).height + : this.workbenchGrid.getViewSize(this.panelPartView).width; this.stateModel.setInitializationValue(LayoutStateKeys.PANEL_SIZE, panelSize as number); // Auxiliary Bar Size @@ -1631,8 +1636,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.workbenchGrid.resizeView(this.panelPartView, { - width: viewSize.width + (this.getPanelPosition() !== Position.BOTTOM ? sizeChangePxWidth : 0), - height: viewSize.height + (this.getPanelPosition() !== Position.BOTTOM ? 0 : sizeChangePxHeight) + width: viewSize.width + (isHorizontal(this.getPanelPosition()) ? 0 : sizeChangePxWidth), + height: viewSize.height + (isHorizontal(this.getPanelPosition()) ? sizeChangePxHeight : 0) }); break; @@ -1766,8 +1771,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private adjustPartPositions(sideBarPosition: Position, panelAlignment: PanelAlignment, panelPosition: Position): void { // Move activity bar and side bars - const sideBarSiblingToEditor = panelPosition !== Position.BOTTOM || !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); - const auxiliaryBarSiblingToEditor = panelPosition !== Position.BOTTOM || !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); + const isPanelVertical = !isHorizontal(panelPosition); + const sideBarSiblingToEditor = isPanelVertical || !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); + const auxiliaryBarSiblingToEditor = isPanelVertical || !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); const preMovePanelWidth = !this.isVisible(Parts.PANEL_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) ?? this.panelPartView.minimumWidth) : this.workbenchGrid.getViewSize(this.panelPartView).width; const preMovePanelHeight = !this.isVisible(Parts.PANEL_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) ?? this.panelPartView.minimumHeight) : this.workbenchGrid.getViewSize(this.panelPartView).height; const preMoveSideBarSize = !this.isVisible(Parts.SIDEBAR_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.sideBarPartView) ?? this.sideBarPartView.minimumWidth) : this.workbenchGrid.getViewSize(this.sideBarPartView).width; @@ -1793,7 +1799,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // We moved all the side parts based on the editor and ignored the panel // Now, we need to put the panel back in the right position when it is next to the editor - if (panelPosition !== Position.BOTTOM) { + if (isPanelVertical) { this.workbenchGrid.moveView(this.panelPartView, preMovePanelWidth, this.editorPartView, panelPosition === Position.LEFT ? Direction.Left : Direction.Right); this.workbenchGrid.resizeView(this.panelPartView, { height: preMovePanelHeight as number, @@ -1820,8 +1826,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi setPanelAlignment(alignment: PanelAlignment, skipLayout?: boolean): void { - // Panel alignment only applies to a panel in the bottom position - if (this.getPanelPosition() !== Position.BOTTOM) { + // Panel alignment only applies to a panel in the top/bottom position + if (!isHorizontal(this.getPanelPosition())) { this.setPanelPosition(Position.BOTTOM); } @@ -1917,7 +1923,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const isMaximized = this.isPanelMaximized(); if (!isMaximized) { if (this.isVisible(Parts.PANEL_PART)) { - if (panelPosition === Position.BOTTOM) { + if (isHorizontal(panelPosition)) { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT, size.height); } else { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH, size.width); @@ -1928,8 +1934,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } else { this.setEditorHidden(false); this.workbenchGrid.resizeView(this.panelPartView, { - width: panelPosition === Position.BOTTOM ? size.width : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH), - height: panelPosition === Position.BOTTOM ? this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT) : size.height + width: isHorizontal(panelPosition) ? size.width : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH), + height: isHorizontal(panelPosition) ? this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT) : size.height }); } @@ -1939,7 +1945,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private panelOpensMaximized(): boolean { // The workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - if (this.getPanelAlignment() !== 'center' && this.getPanelPosition() === Position.BOTTOM) { + if (this.getPanelAlignment() !== 'center' && isHorizontal(this.getPanelPosition())) { return false; } @@ -2017,7 +2023,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi isPanelMaximized(): boolean { // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment - return (this.getPanelAlignment() === 'center' || this.getPanelPosition() !== Position.BOTTOM) && !this.isVisible(Parts.EDITOR_PART, mainWindow); + return (this.getPanelAlignment() === 'center' || !isHorizontal(this.getPanelPosition())) && !this.isVisible(Parts.EDITOR_PART, mainWindow); } getSideBarPosition(): Position { @@ -2093,14 +2099,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Save the current size of the panel for the new orthogonal direction // If moving down, save the width of the panel // Otherwise, save the height of the panel - if (position === Position.BOTTOM) { + if (isHorizontal(position)) { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH, size.width); - } else if (positionFromString(oldPositionValue) === Position.BOTTOM) { + } else if (isHorizontal(positionFromString(oldPositionValue))) { this.stateModel.setRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT, size.height); } } - if (position === Position.BOTTOM && this.getPanelAlignment() !== 'center' && editorHidden) { + if (isHorizontal(position) && this.getPanelAlignment() !== 'center' && editorHidden) { this.toggleMaximizedPanel(); editorHidden = false; } @@ -2112,6 +2118,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (position === Position.BOTTOM) { this.workbenchGrid.moveView(this.panelPartView, editorHidden ? size.height : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT), this.editorPartView, Direction.Down); + } else if (position === Position.TOP) { + this.workbenchGrid.moveView(this.panelPartView, editorHidden ? size.height : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_HEIGHT), this.editorPartView, Direction.Up); } else if (position === Position.RIGHT) { this.workbenchGrid.moveView(this.panelPartView, editorHidden ? size.width : this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_LAST_NON_MAXIMIZED_WIDTH), this.editorPartView, Direction.Right); } else { @@ -2129,7 +2137,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.setAuxiliaryBarHidden(true); } - if (position === Position.BOTTOM) { + if (isHorizontal(position)) { this.adjustPartPositions(this.getSideBarPosition(), this.getPanelAlignment(), position); } @@ -2238,17 +2246,20 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const auxiliaryBarSize = this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN) ? 0 : nodes.auxiliaryBar.size; const panelSize = this.stateModel.getInitializationValue(LayoutStateKeys.PANEL_SIZE) ? 0 : nodes.panel.size; + const panelPostion = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION); + const sideBarPosition = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_POSITON); + const result = [] as ISerializedNode[]; - if (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) !== Position.BOTTOM) { + if (!isHorizontal(panelPostion)) { result.push(nodes.editor); nodes.editor.size = availableWidth - activityBarSize - sideBarSize - panelSize - auxiliaryBarSize; - if (this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_POSITION) === Position.RIGHT) { + if (panelPostion === Position.RIGHT) { result.push(nodes.panel); } else { result.splice(0, 0, nodes.panel); } - if (this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_POSITON) === Position.LEFT) { + if (sideBarPosition === Position.LEFT) { result.push(nodes.auxiliaryBar); result.splice(0, 0, nodes.sideBar); result.splice(0, 0, nodes.activityBar); @@ -2259,18 +2270,20 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } else { const panelAlignment = this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_ALIGNMENT); - const sideBarPosition = this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_POSITON); const sideBarNextToEditor = !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); const auxiliaryBarNextToEditor = !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); const editorSectionWidth = availableWidth - activityBarSize - (sideBarNextToEditor ? 0 : sideBarSize) - (auxiliaryBarNextToEditor ? 0 : auxiliaryBarSize); + + const editorNodes = this.arrangeEditorNodes({ + editor: nodes.editor, + sideBar: sideBarNextToEditor ? nodes.sideBar : undefined, + auxiliaryBar: auxiliaryBarNextToEditor ? nodes.auxiliaryBar : undefined + }, availableHeight - panelSize, editorSectionWidth); + result.push({ type: 'branch', - data: [this.arrangeEditorNodes({ - editor: nodes.editor, - sideBar: sideBarNextToEditor ? nodes.sideBar : undefined, - auxiliaryBar: auxiliaryBarNextToEditor ? nodes.auxiliaryBar : undefined - }, availableHeight - panelSize, editorSectionWidth), nodes.panel], + data: panelPostion === Position.BOTTOM ? [editorNodes, nodes.panel] : [nodes.panel, editorNodes], size: editorSectionWidth }); @@ -2413,7 +2426,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the panel is visible' }; statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the status bar is visible' }; sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the primary side bar is on the left or right' }; - panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the panel is on the bottom, left, or right' }; + panelPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the panel is on the top, bottom, left, or right' }; }; const layoutDescriptor: StartupLayoutEvent = { @@ -2621,7 +2634,7 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === Position.BOTTOM ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? isHorizontal(LayoutStateKeys.PANEL_POSITION.defaultValue)) ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; // Apply all defaults diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 35f856b931aa6..6c9dbb9a0c902 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 4610ddff2c38f..97f3861079f31 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 f42d3307fb75d..b40341d217ce1 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 16c40e836f214..3a2422d4c9985 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/auxiliarybar/media/auxiliaryBarPart.css b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css index b1e39af1e5898..781c090fbe57e 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css +++ b/src/vs/workbench/browser/parts/auxiliarybar/media/auxiliaryBarPart.css @@ -52,7 +52,7 @@ .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, .monaco-workbench .part.auxiliarybar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { position: absolute; - left: 6px; /* place icon in center */ + left: 5px; /* place icon in center */ } .monaco-workbench .part.auxiliarybar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index de10721d07cac..24b1b97b7dd21 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 c86d7fd78f5cb..cbc4783066ece 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 e2fefb174e649..6bba2fcc4c5a8 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/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 67d4b7173d42a..eb9d1534e1137 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); } } @@ -564,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); @@ -636,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 }); @@ -656,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 }); @@ -686,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(); @@ -1139,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); + } } } @@ -2508,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 c8670de6fb066..7f137ca5244c1 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; - - 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 - } +export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, resolvedContext: IResolvedEditorCommandsContext): void { + if (!resolvedContext.groupedEditors.length) { + return; + } - // 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); - - 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)); + const resolvedContext = resolveCommandsContext(accessor, args); + const preserveFocus = resolvedContext.preserveFocus; - 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); - - let group: IEditorGroup | undefined; - if (commandsContext && typeof commandsContext.groupId === 'number') { - group = editorGroupService.getGroup(commandsContext.groupId); - } else { - group = editorGroupService.activeGroup; - } + const commandsContext = resolveCommandsContext(accessor, args); - 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); - - const editorsToClose = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).filter(editor => !editorsToKeep.includes(editor)); - - for (const editorToKeep of editorsToKeep) { - if (editorToKeep) { - group.pinEditor(editorToKeep); - } - } + handler: (accessor, ...args: unknown[]) => { + const resolvedContext = resolveCommandsContext(accessor, args); - await group.closeEditors(editorsToClose, { preserveFocus: context?.preserveFocus }); + 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 editors) { + if (editorToKeep) { + group.pinEditor(editorToKeep); + } } + + 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; - } + 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); - } + 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 ?? '' - }); + 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 { editors } = resolvedContext.groupedEditors[0]; - 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); + 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 0000000000000..ee5957ddfa093 --- /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 463b527e3b667..b11c66e605773 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 6c8c42ec330a3..41e4e3ccc241b 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'; @@ -259,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); @@ -355,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; } @@ -558,14 +560,15 @@ 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 @@ -576,6 +579,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.focus(); } }); + + // Restore editors in title control + this.titleControl.openEditors(this.editors); + + return result; } //#region event handling @@ -827,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 5d638871273ab..ef1b25dd681c5 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 d785a5137981e..fbdfd7daf7d50 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -1103,6 +1103,10 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { openVerticalPosition = Position.BOTTOM; } + if (e.eventData.clientY < boundingRect.top + proximity) { + openVerticalPosition = Position.TOP; + } + if (horizontalOpenerTimeout && openHorizontalPosition !== lastOpenHorizontalPosition) { clearTimeout(horizontalOpenerTimeout); horizontalOpenerTimeout = undefined; diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index e505a3fd466ee..18123131e5b58 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -761,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 def52c5b61ea8..e434679f445e6 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 83016b30a463d..845160784b16a 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -1453,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 588c334451a9b..9007f132a8e7c 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -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 83823d3ec8fa8..69eb8b7726262 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 c3d7a0cce9dab..2dab828b50ab6 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 52baa5324f725..ecb5cad2d8b68 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -73,13 +73,9 @@ .monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar > .monaco-action-bar .action-item.icon { height: 35px; /* matches height of composite container */ - padding: 0 5px; + padding: 0 3px; } -.monaco-workbench .pane-composite-part > .title > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon, -.monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container >.composite-bar .monaco-action-bar .action-label.codicon { - font-size: 18px; -} .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon), .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .action-label:not(.codicon) { @@ -229,14 +225,20 @@ right: 0px; font-size: 9px; font-weight: 600; - min-width: 13px; - height: 13px; - line-height: 13px; + min-width: 12px; + height: 12px; + line-height: 12px; padding: 0 2px; border-radius: 16px; 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/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 5d29de1726af6..943136732c1ef 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 65ad1afe6cc09..c1fda244291e9 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 68e797d69e125..cf29cbed9f181 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/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 40a5ee28faf62..e1c147d8e88a3 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -17,6 +17,15 @@ border-top-width: 0; /* no border when main editor area is hiden */ } +.monaco-workbench .part.panel.top { + border-bottom-width: 1px; + border-bottom-style: solid; +} + +.monaco-workbench.nomaineditorarea .part.panel.top { + border-bottom-width: 0; /* no border when main editor area is hiden */ +} + .monaco-workbench .part.panel.right { border-left-width: 1px; border-left-style: solid; @@ -81,3 +90,11 @@ display: inline-block; transform: rotate(90deg); } + +/* Rotate icons when panel is on left */ +.monaco-workbench .part.basepanel.top .title-actions .codicon-split-horizontal::before, +.monaco-workbench .part.basepanel.top .global-actions .codicon-panel-maximize::before, +.monaco-workbench .part.basepanel.top .global-actions .codicon-panel-restore::before { + display: inline-block; + transform: rotate(180deg); +} diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 534db0283a72f..3e97968289db2 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -8,7 +8,7 @@ import { localize, localize2 } from 'vs/nls'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuId, MenuRegistry, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, PanelAlignment, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; +import { ActivityBarPosition, isHorizontal, IWorkbenchLayoutService, LayoutSettings, PanelAlignment, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/contextkeys'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { Codicon } from 'vs/base/common/codicons'; @@ -99,6 +99,7 @@ const PositionPanelActionId = { LEFT: 'workbench.action.positionPanelLeft', RIGHT: 'workbench.action.positionPanelRight', BOTTOM: 'workbench.action.positionPanelBottom', + TOP: 'workbench.action.positionPanelTop' }; const AlignPanelActionId = { @@ -136,6 +137,7 @@ function createAlignmentPanelActionConfig(id: string, title: ICommandActionTitle const PositionPanelActionConfigs: PanelActionConfig[] = [ + createPositionPanelActionConfig(PositionPanelActionId.TOP, localize2('positionPanelTop', "Move Panel To Top"), localize('positionPanelTopShort', "Top"), Position.TOP), createPositionPanelActionConfig(PositionPanelActionId.LEFT, localize2('positionPanelLeft', "Move Panel Left"), localize('positionPanelLeftShort', "Left"), Position.LEFT), createPositionPanelActionConfig(PositionPanelActionId.RIGHT, localize2('positionPanelRight', "Move Panel Right"), localize('positionPanelRightShort', "Right"), Position.RIGHT), createPositionPanelActionConfig(PositionPanelActionId.BOTTOM, localize2('positionPanelBottom', "Move Panel To Bottom"), localize('positionPanelBottomShort', "Bottom"), Position.BOTTOM), @@ -158,7 +160,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 4 }); -PositionPanelActionConfigs.forEach(positionPanelAction => { +PositionPanelActionConfigs.forEach((positionPanelAction, index) => { const { id, title, shortLabel, value, when } = positionPanelAction; registerAction2(class extends Action2 { @@ -182,7 +184,7 @@ PositionPanelActionConfigs.forEach(positionPanelAction => { title: shortLabel, toggled: when.negate() }, - order: 5 + order: 5 + index }); }); @@ -280,7 +282,7 @@ registerAction2(class extends Action2 { tooltip: localize('maximizePanel', "Maximize Panel Size"), category: Categories.View, f1: true, - icon: maximizeIcon, + icon: maximizeIcon, // This is being rotated in CSS depending on the panel position // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment precondition: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), PanelPositionContext.notEqualsTo('bottom')), toggled: { condition: PanelMaximizedContext, icon: restoreIcon, tooltip: localize('minimizePanel', "Restore Panel Size") }, @@ -296,7 +298,7 @@ registerAction2(class extends Action2 { run(accessor: ServicesAccessor) { const layoutService = accessor.get(IWorkbenchLayoutService); const notificationService = accessor.get(INotificationService); - if (layoutService.getPanelAlignment() !== 'center' && layoutService.getPanelPosition() === Position.BOTTOM) { + if (layoutService.getPanelAlignment() !== 'center' && isHorizontal(layoutService.getPanelPosition())) { notificationService.warn(localize('panelMaxNotSupported', "Maximizing the panel is only supported when it is center aligned.")); return; } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index e8e8b49bce536..899d97b4cda24 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -112,6 +112,7 @@ export class PanelPart extends AbstractPaneCompositePart { const borderColor = this.getColor(PANEL_BORDER) || this.getColor(contrastBorder) || ''; container.style.borderLeftColor = borderColor; container.style.borderRightColor = borderColor; + container.style.borderBottomColor = borderColor; const title = this.getTitleArea(); if (title) { @@ -149,14 +150,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(), @@ -168,10 +167,16 @@ export class PanelPart extends AbstractPaneCompositePart { override layout(width: number, height: number, top: number, left: number): void { let dimensions: Dimension; - if (this.layoutService.getPanelPosition() === Position.RIGHT) { - dimensions = new Dimension(width - 1, height); // Take into account the 1px border when layouting - } else { - dimensions = new Dimension(width, height); + switch (this.layoutService.getPanelPosition()) { + case Position.RIGHT: + dimensions = new Dimension(width - 1, height); // Take into account the 1px border when layouting + break; + case Position.TOP: + dimensions = new Dimension(width, height - 1); // Take into account the 1px border when layouting + break; + default: + dimensions = new Dimension(width, height); + break; } // Layout contents diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index d4be01b7f2f4c..2c94078993b5f 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -78,7 +78,7 @@ .monaco-workbench .part.sidebar > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before, .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item .action-label::before { position: absolute; - left: 6px; /* place icon in center */ + left: 5px; /* place icon in center */ } .monaco-workbench .part.sidebar > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator:before, diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index d6897bf02d7c0..b30d2bee088aa 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 07dd5640c0c0a..d458ce91e7918 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 8fd7e353217b3..8c0ecf356a2ec 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 f938ea7b3535e..bac67111eae1b 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 59b379f497d60..88d5435936b9a 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/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 80c99f1557d2f..f610512e84f7e 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'; @@ -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); } @@ -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 6d6125e4f5c17..f428de9bffea5 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 db78e940c5d92..5b82b9f971b00 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); @@ -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 3331c892d0c29..724a67b931a98 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 c62f1b0ab4745..28a82a629df73 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 47fc7c4e9a431..01df5c12a696c 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -39,7 +39,7 @@ import { IAddedViewDescriptorRef, ICustomViewDescriptor, IView, IViewContainerMo import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchLayoutService, LayoutSettings, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { isHorizontal, IWorkbenchLayoutService, LayoutSettings } from 'vs/workbench/services/layout/browser/layoutService'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; export const ViewsSubMenu = new MenuId('Views'); @@ -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'); })); @@ -625,8 +625,9 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { case ViewContainerLocation.Sidebar: case ViewContainerLocation.AuxiliaryBar: return Orientation.VERTICAL; - case ViewContainerLocation.Panel: - return this.layoutService.getPanelPosition() === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; + case ViewContainerLocation.Panel: { + return isHorizontal(this.layoutService.getPanelPosition()) ? Orientation.HORIZONTAL : Orientation.VERTICAL; + } } return Orientation.VERTICAL; @@ -1100,10 +1101,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) { return false; } - if (this.paneItems[0].pane.titleDescription) { - // Don't merge a view with a titleDescription. See #166000 - return false; - } if (!this.areExtensionsReady) { if (this.visibleViewsCountFromCache === undefined) { return this.paneItems[0].pane.isExpanded(); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 8494c963f3fa2..d83ac066c6e21 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -6,7 +6,7 @@ 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 { 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'; @@ -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].*' }, @@ -509,9 +516,9 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.panel.defaultLocation': { 'type': 'string', - 'enum': ['left', 'bottom', 'right'], + 'enum': ['left', 'bottom', 'top', 'right'], 'default': 'bottom', - 'description': localize('panelDefaultLocation', "Controls the default location of the panel (Terminal, Debug Console, Output, Problems) in a new workspace. It can either show at the bottom, right, or left of the editor area."), + 'description': localize('panelDefaultLocation', "Controls the default location of the panel (Terminal, Debug Console, Output, Problems) in a new workspace. It can either show at the bottom, top, right, or left of the editor area."), }, 'workbench.panel.opensMaximized': { 'type': 'string', diff --git a/src/vs/workbench/common/configuration.ts b/src/vs/workbench/common/configuration.ts index 01b4d2b510d57..f4aff57ea8595 100644 --- a/src/vs/workbench/common/configuration.ts +++ b/src/vs/workbench/common/configuration.ts @@ -234,7 +234,7 @@ export class DynamicWorkbenchSecurityConfiguration extends Disposable implements } } -const CONFIG_NEW_WINDOW_PROFILE = 'window.newWindowProfile'; +export const CONFIG_NEW_WINDOW_PROFILE = 'window.newWindowProfile'; export class DynamicWindowConfiguration extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 97937218bbb36..d02f9e0e89dda 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 aaf1452c25a4a..f7e23fae0a270 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 02aff158f4fee..d3d6adb4ff563 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 3412f81c3bfbc..1e6aba052fd54 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_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_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', 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_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_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_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', 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_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_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', 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_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_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_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_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', 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_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_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', null, localize('panelStickyScrollBorder', "Border 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_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_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_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', 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', 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_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_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_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', 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', 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_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_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_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', 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', 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_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_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_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', 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', 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 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 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_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_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', 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', SIDE_BAR_BACKGROUND, localize('sideBarStickyScrollBackground', "Background 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_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', 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_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_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_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_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', editorWidgetBackground, localize('notificationsBackground', "Notifications background 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', 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_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_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_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_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', 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', 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', 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 2622ffd8dafe2..fa4e47467d236 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,118 +176,77 @@ const configuration: IConfigurationNode = { type: 'boolean', default: true }, - 'accessibility.signalOptions': { - description: 'Configures the behavior of signals (audio cues and announcements) in the workbench. Includes volume, debounce position changes, and delays for different types of signals.', - 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, - }, - 'experimental.delays': { - 'type': 'object', - 'additionalProperties': false, - 'properties': { - '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, - 'default': 3000 - }, - 'sound': { - 'description': localize('accessibility.signalOptions.delays.general.sound', "The delay in milliseconds before a sound is played."), - 'type': 'number', - 'minimum': 0, - 'default': 400 - } - }, - }, - '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 - } - }, - }, - '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 - } - }, - }, - }, - 'default': { - 'general': { - 'announcement': 3000, - 'sound': 400 - }, - 'warningAtPosition': { - 'announcement': 3000, - 'sound': 1000 - }, - 'errorAtPosition': { - 'announcement': 3000, - 'sound': 1000 - } - } + 'sound': { + 'description': localize('accessibility.signalOptions.delays.general.sound', "The delay in milliseconds before a sound is played."), + 'type': 'number', + 'minimum': 0, + 'default': 400 + } + }, + '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 + } }, - '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.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'] + 'tags': ['accessibility'] }, 'accessibility.signals.lineHasBreakpoint': { ...signalFeatureBase, @@ -698,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, + }, } }; @@ -804,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 }] ]; } @@ -816,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 }] ]; } @@ -829,12 +796,18 @@ Registry.as(WorkbenchExtensions.ConfigurationMi .registerConfigurationMigrations([{ key: 'accessibility.signalOptions', migrateFn: (value, accessor) => { - const delays = value.delays; - if (!delays) { - return []; - } + 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: { ...value, 'experimental.delays': delays, 'delays': undefined } }], + ['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 }], ]; } }]); @@ -843,10 +816,9 @@ Registry.as(WorkbenchExtensions.ConfigurationMi Registry.as(WorkbenchExtensions.ConfigurationMigration) .registerConfigurationMigrations([{ key: 'accessibility.signals.sounds.volume', - migrateFn: (volume, accessor) => { - const debouncePositionChanges = getDebouncePositionChangesFromConfig(accessor); + migrateFn: (value) => { return [ - ['accessibility.signalOptions', { value: debouncePositionChanges !== undefined ? { volume, debouncePositionChanges } : { volume } }], + ['accessibility.signalOptions.volume', { value }], ['accessibility.signals.sounds.volume', { value: undefined }] ]; } @@ -855,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/accessibilitySignals/browser/accessibilitySignal.contribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/accessibilitySignal.contribution.ts index cb298c79733ba..deb7708581959 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 336832eb16cf3..f0bc218c0c1de 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 de1c2f798afa7..f5dddf8ada492 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 43fb32eed293a..0000000000000 --- 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/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 36ef150418ae4..a24720d5b7518 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 { ButtonBar } from 'vs/base/browser/ui/button/button'; +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 { 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 { Mutable } from 'vs/base/common/types'; 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 '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 { 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 { 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 { 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 { 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 { 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 { 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 { Mutable } from 'vs/base/common/types'; -import { IResourceDiffEditorInput } from 'vs/workbench/common/editor'; -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 { IHoverService } from 'vs/platform/hover/browser/hover'; +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 40fbb606f8b96..44c687a6deea5 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 22c507ca0b336..dc00b4d61d275 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 a05e30502cc88..50cd0b41f8cce 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/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index bf426bec1b809..29731511a132a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -15,16 +15,17 @@ 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 { IQuickInputButton, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; +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 { 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'; @@ -102,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: { @@ -121,12 +121,11 @@ 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 editorService = accessor.get(IEditorService); - const items = chatService.getHistory(); const openInEditorButton: IQuickInputButton = { iconClass: ThemeIcon.asClassName(Codicon.file), @@ -140,25 +139,30 @@ class ChatHistoryAction extends ViewAction { interface IChatPickerItem extends IQuickPickItem { chat: IChatDetail; } - const picks: IChatPickerItem[] = items.map((i): IChatPickerItem => ({ - label: i.title, - chat: i, - buttons: [ - openInEditorButton, - deleteButton - ] - })); + + 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 = picks; + 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 = picks.filter(i => i !== context.item); + picker.items = getPicks(); } })); store.add(picker.onDidAccept(async () => { @@ -226,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); + } + }); + }); } }); @@ -288,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 00ffcaed8c73f..cd85340c42a32 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 b74d1b38b9c9d..1c6572f3415d3 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -81,7 +81,7 @@ export function registerNewChatActions() { if (isChatViewTitleActionContext(context)) { // Is running in the Chat view title announceChatCleared(accessibilitySignalService); - context.chatView.clear(); + context.chatView.widget.clear(); context.chatView.widget.focusInput(); } else { // Is running from f1 or keybinding diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 6d9b8c58a4314..378d6857e489b 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, } }); } @@ -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 01a8f050bac9d..ee4b0beba482b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -30,9 +30,14 @@ import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVari 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 | IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickAccessQuickPickItem; @@ -77,6 +82,58 @@ export interface IQuickAccessQuickPickItem extends IQuickPickItem { 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'; @@ -231,7 +288,21 @@ class AttachContextAction extends Action2 { prefix: SymbolsQuickAccessProvider.PREFIX }); - this._show(quickInputService, commandService, widget, quickPickItems); + function extractTextFromIconLabel(label: string | undefined): string { + if (!label) { + return ''; + } + const match = label.match(/\$\([^\)]+\)\s*(.+)/); + return match ? match[1] : label; + } + + 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 = '') { 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 0000000000000..4ab735e2b4e3c --- /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 5a6574db112f7..b7d76a5a22719 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 195ab2b3aa10e..d04e49d62188c 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 4a2455ea71fda..9338bf5b51b10 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 44064deca5660..9356ca139e570 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -48,6 +48,7 @@ import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNa 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'; @@ -58,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); @@ -98,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, @@ -160,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); })); @@ -168,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(); @@ -237,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(); @@ -250,6 +269,7 @@ registerChatExportActions(); registerMoveActions(); registerNewChatActions(); registerChatContextActions(); +registerChatDeveloperActions(); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); @@ -262,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 48844b393d850..99bb40c2ca473 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 5eb8edf7f6d7a..ecfaa1653f1c4 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 d35b283ae34de..06e0d0e292f11 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -34,7 +34,7 @@ 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; @@ -44,4 +44,3 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi status(plainTextResponse + errorDetails); } } - diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 9e48ba6bc1864..78f34762cf01a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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'; @@ -40,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; @@ -121,7 +120,7 @@ export class ChatAgentHover extends Disposable { } } -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/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts new file mode 100644 index 0000000000000..ebcc16d2ca57f --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; +import { Emitter } from 'vs/base/common/event'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ResourceLabels } from 'vs/workbench/browser/labels'; +import { URI } from 'vs/base/common/uri'; +import { FileKind } from 'vs/platform/files/common/files'; +import { Range } from 'vs/editor/common/core/range'; +import { basename, dirname } from 'vs/base/common/path'; +import { localize } from 'vs/nls'; + +export class ChatAttachmentsContentPart extends Disposable { + private readonly attachedContextDisposables = this._register(new DisposableStore()); + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); + + constructor( + private readonly variables: IChatRequestVariableEntry[], + public readonly domNode: HTMLElement = dom.$('.chat-attached-context'), + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.initAttachedContext(domNode); + } + + private initAttachedContext(container: HTMLElement) { + dom.clearNode(container); + this.attachedContextDisposables.clear(); + dom.setVisibility(Boolean(this.variables.length), this.domNode); + + this.variables.forEach((attachment, index) => { + const widget = dom.append(container, dom.$('.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; + const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + + if (file) { + 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, + }); + widget.ariaLabel = ariaLabel; + widget.tabIndex = 0; + } else { + const attachmentLabel = attachment.fullName ?? attachment.name; + label.setLabel(attachmentLabel, undefined); + + widget.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); + widget.tabIndex = 0; + } + }); + } +} + 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 0000000000000..d13bbdff23b40 --- /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 0000000000000..3893117fd20ae --- /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 0000000000000..3748611876266 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.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 { 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 buttons = confirmation.buttons + ? confirmation.buttons.map(button => ({ + label: button, + data: confirmation.data + })) + : [ + { label: localize('accept', "Accept"), data: confirmation.data }, + { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, + ]; + const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, buttons)); + 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 0000000000000..80e3c44a05603 --- /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 0000000000000..29a2573374b08 --- /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 0000000000000..6545fdfe9adbf --- /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 0000000000000..b1305e8e160ae --- /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 0000000000000..a1a03e1cae157 --- /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 0000000000000..5f4e6556c5cb2 --- /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 0000000000000..8742d2da602e2 --- /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 0000000000000..3fd0b9fb239f6 --- /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 1da5d57b810f0..94fe53297a8de 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 90acf8cf0b104..9fcc9fb8e50cd 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'; @@ -53,9 +57,6 @@ import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatHistoryEntry, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { basename, dirname } from 'vs/base/common/path'; const $ = dom.$; @@ -130,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; @@ -156,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(); @@ -165,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)) { @@ -175,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) { @@ -184,15 +194,37 @@ 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); + updateState(inputState: Object): void { + if (this.inHistoryNavigation) { + return; + } + + const newEntry = { text: this._inputEditor.getValue(), state: inputState }; - if (typeof inputValue === 'string') { - this.setValue(inputValue); + 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 { this._onDidChangeVisibility.fire(visible); } @@ -202,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); @@ -235,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() { @@ -253,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 { @@ -279,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(); @@ -305,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; @@ -343,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); @@ -379,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, { @@ -509,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 { @@ -530,7 +571,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.inputPartVerticalPadding); - this.followupsContainer.style.width = `${width}px`; + const followupsWidth = width - data.inputPartHorizontalPadding; + this.followupsContainer.style.width = `${followupsWidth}px`; this._inputPartHeight = data.followupsHeight + inputEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; @@ -567,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 9d1f8781ed6eb..6bb4cc041f845 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, isEqual } 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,52 +32,54 @@ 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 { IChatRequestVariableEntry, 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'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { ChatAttachmentsContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart'; 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; @@ -105,7 +90,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; @@ -143,8 +130,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); - private _usedReferencesEnabled = false; - constructor( editorOptions: ChatEditorOptions, private readonly location: ChatAgentLocation, @@ -155,14 +140,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 { @@ -199,21 +173,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; } @@ -312,7 +306,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.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 +318,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 +407,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 (isRequestVM(element) && element.variables.length) { + const newPart = this.renderAttachments(element.variables, templateData); + if (newPart) { + templateData.value.appendChild(newPart.domNode); + templateData.elementDisposables.add(newPart); + } + } 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 +528,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 +541,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,668 +571,317 @@ 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 }; - } - } - }); + this.basicRenderElement(element, index, templateData); + return true; + } - isFullyRendered = partsToRender.filter((p) => !('isSettled' in p) || !p.isSettled).length === 0 && !somePartIsNotFullyRendered; + 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 && element.isComplete) { + if (isFullyRendered) { + if (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}`); + this.traceLayout('doNextProgressiveRender', `END progressive render, index=${index} and clearing renderData, response is complete`); 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; } + + // Nothing new to render, not done, keep waiting + this.traceLayout('doNextProgressiveRender', 'caught up with the stream- no new content to render'); + return false; } - // Some render happened - update the height + // 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 }; + 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 { + // This method can throw HierarchyRequestError + alreadyRenderedPart.domNode.replaceWith(newPart.domNode); + } catch (err) { + this.logService.error('ChatListItemRenderer#renderChatContentDiff: error replacing part', err); + } + } else { + templateData.value.appendChild(newPart.domNode); + } - 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); + renderedParts[index] = newPart; + } else if (alreadyRenderedPart) { + alreadyRenderedPart.domNode.remove(); } - 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); - })); + /** + * 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 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 renderableResponse = annotateSpecialMarkdownContent(element.response.value); - 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); + 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 }); + } - return { - element: container, - dispose: () => { - listDisposables.dispose(); + for (let i = 0; i < renderableResponse.length; i++) { + const part = renderableResponse[i]; + if (numNeededWords <= 0) { + break; } - }; - } - private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { - element.ariaLabel = expanded ? localize('usedReferencesExpanded', "{0}, expanded", label) : localize('usedReferencesCollapsed', "{0}, collapsed", label); - } + 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) }); + } - private renderProgressTask(task: IChatTask, showSpinner: boolean, element: ChatTreeItem, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { - if (!isResponseVM(element)) { - return; + 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); + } } - 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 }; + 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 this.renderProgressMessage(task, showSpinner); + return partsToRender; } - 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 getDataForProgressiveRender(element: IChatResponseViewModel) { + const renderData = element.renderData ?? { lastRenderTime: 0, renderedWordCount: 0 }; - 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 - }; - } + const rate = this.getProgressiveRenderRate(element); + const numWordsToRender = renderData.lastRenderTime === 0 ? + 1 : + renderData.renderedWordCount + + // Additional words to render beyond what's already rendered + Math.floor((Date.now() - renderData.lastRenderTime) / 1000 * rate); - 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(); } + numWordsToRender, + rate }; } - 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); - } + private diff(renderedParts: ReadonlyArray, 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 { - element: confirmationWidget.domNode, - dispose() { store.dispose(); } - }; + return diff; } - 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; + 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); } - const store = new DisposableStore(); - const cts = new CancellationTokenSource(); - - let isDisposed = false; - store.add(toDisposable(() => { - isDisposed = true; - cts.dispose(true); - })); + return undefined; + } - const ref = this._diffEditorPool.get(); + 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); - // 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); + treePart.addDisposable(treePart.onDidChangeHeight(() => { 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 - ); - const modRef = await this.textModelService.createModelReference(modified.uri); - store.add(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; - } - } + if (isResponseVM(context.element)) { + const fileTreeFocusInfo = { + treeDataId: data.uri.toString(), + treeIndex: treeDataIndex, + focus() { + treePart.domFocus(); } + }; - for (const edits of editGroups) { - modified.pushEditOperations(null, edits, () => null); - } + // 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); + })); - return { - modified, - original, - originalSha1 - } satisfies ICodeCompareBlockDiffData; - })() - }; - ref.object.render(data, this._currentLayoutWidth, cts.token); + 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 { - element: ref.object.element, - dispose() { - store.dispose(); - ref.dispose(); - }, - }; + return treePart; } - 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)) { - 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; - } + 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); + })); - 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), - }); + return referencesPart; + } - if (isResponseVM(element)) { - this.codeBlocksByResponseId.set(element.id, codeblocks); - disposables.add(toDisposable(() => this.codeBlocksByResponseId.delete(element.id))); + private renderProgressTask(task: IChatTask, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { + if (!isResponseVM(context.element)) { + return; } - disposables.add(this.markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element)); + const taskPart = this.instantiationService.createInstance(ChatTaskContentPart, task, this._contentReferencesListPool, this.renderer, context); + taskPart.addDisposable(taskPart.onDidChangeHeight(() => { + this.updateItemHeight(templateData); + })); + return taskPart; + } - orderedDisposablesList.reverse().forEach(d => disposables.add(d)); - return { - element: result.element, - dispose() { - result.dispose(); - disposables.dispose(); - } - }; + 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 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 }); - } + private renderAttachments(variables: IChatRequestVariableEntry[], templateData: IChatListItemTemplate) { + return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, undefined); + } - editorInfo.render(data, this._currentLayoutWidth, this.rendererOptions.editableCodeBlock); + 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 ref; + return textEditPart; } - private getDataForProgressiveRender(element: IChatResponseViewModel, data: IMarkdownString, renderData: Pick): IWordCountResult & { rate: number } | undefined { - const rate = this.getProgressiveRenderRate(element); - const numWordsToRender = renderData.lastRenderTime === 0 ? - 1 : - renderData.renderedWordCount + - // Additional words to render beyond what's already rendered - Math.floor((Date.now() - renderData.lastRenderTime) / 1000 * rate); + 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); + })); - if (numWordsToRender === renderData.renderedWordCount) { - return undefined; - } + 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]); + } + })); - return { - ...getNWords(data.value, numWordsToRender), - rate - }; + 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(); } @@ -1270,469 +920,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 25c4ac91e44bd..9af542462c305 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 3e4b6a9349f50..4f08d298d9c44 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', @@ -73,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 43eb5d2be7bbe..a1c6d7a732d09 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -63,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', @@ -181,11 +185,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - 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})`); - continue; - } - for (const providerDescriptor of extension.value) { if (!providerDescriptor.name.match(/^[\w0-9_-]+$/)) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with invalid name: ${providerDescriptor.name}. Name must match /^[\\w0-9_-]+$/.`); @@ -223,11 +222,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { store.add(this.registerDefaultParticipantView(providerDescriptor)); } - if (providerDescriptor.when && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`); - continue; - } - store.add(this._chatAgentService.registerAgent( providerDescriptor.id, { @@ -245,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 009d9f4c3b103..6d682b5d72e3c 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) { diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 61b4ee1984f6a..cd978afa041e1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -90,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, diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index b0c5440439958..80022029e4079 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 830664edef032..21ad0513b63dc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -36,7 +36,7 @@ import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUE 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,15 @@ 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); @@ -311,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 { @@ -363,6 +395,8 @@ export class ChatWidget extends Disposable implements IChatWidget { }; }); + this._onWillMaybeChangeHeight.fire(); + this.tree.setChildren(null, treeItems, { diffIdentityProvider: { getId: (element) => { @@ -424,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, @@ -538,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, } @@ -552,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); } }); })); @@ -593,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', @@ -646,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]); @@ -692,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); } @@ -727,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]; @@ -918,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 279626daf8218..89596f8974851 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.css +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.css @@ -147,3 +147,17 @@ .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 46e306fd9d2b6..7caa40d96848b 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,36 +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 { basename, 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 { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { toAction } from 'vs/base/common/actions'; +import { ResourceLabel } from 'vs/workbench/browser/labels'; +import { FileKind } from 'vs/platform/files/common/files'; const $ = dom.$; @@ -143,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, @@ -159,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), @@ -189,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 @@ -263,6 +265,11 @@ export class CodeBlockPart extends Disposable { } } + override dispose() { + this.isDisposed = true; + super.dispose(); + } + get uri(): URI | undefined { return this.editor.getModel()?.uri; } @@ -282,6 +289,7 @@ export class CodeBlockPart extends Disposable { HoverController.ID, MessageController.ID, GotoDefinitionAtPositionEditorContribution.ID, + ColorDetector.ID ]) })); } @@ -354,6 +362,9 @@ export class CodeBlockPart extends Disposable { } await this.updateEditor(data); + if (this.isDisposed) { + return; + } this.layout(width); if (editable) { @@ -458,7 +469,7 @@ export interface ICodeCompareBlockData { readonly diffData: Promise; readonly parentContextKeyService?: IContextKeyService; - readonly hideToolbar?: boolean; + // readonly hideToolbar?: boolean; } @@ -468,8 +479,8 @@ export class CodeCompareBlockPart extends Disposable { private readonly contextKeyService: IContextKeyService; private readonly diffEditor: DiffEditorWidget; - private readonly toolbar1: ActionBar; - private readonly toolbar2: MenuWorkbenchToolBar; + private readonly resourceLabel: ResourceLabel; + private readonly toolbar: MenuWorkbenchToolBar; readonly element: HTMLElement; private readonly messageElement: HTMLElement; @@ -498,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), @@ -525,24 +537,16 @@ export class CodeCompareBlockPart extends Disposable { ...this.getEditorOptionsFromConfig(), }); - const toolbarElement = dom.append(this.element, $('.interactive-result-code-block-toolbar')); - - // this.resourceLabel = this._register(scopedInstantiationService.createInstance(ResourceLabel, toolbarElement, { supportIcons: true })); + this.resourceLabel = this._register(scopedInstantiationService.createInstance(ResourceLabel, editorHeader, { supportIcons: true })); - const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(toolbarElement); - const editorScopedInstantiationService = scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])); - this.toolbar1 = this._register(new ActionBar(toolbarElement, {})); - this.toolbar2 = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarElement, menuId, { + 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.toolbar2.onDidChangeDropdownVisibility(e => { - toolbarElement.classList.toggle('force-visibility', e); - })); - this._configureForScreenReader(); this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._configureForScreenReader())); this._register(this.configurationService.onDidChangeConfiguration((e) => { @@ -637,7 +641,7 @@ export class CodeCompareBlockPart extends Disposable { } private _configureForScreenReader(): void { - const toolbarElt = this.toolbar2.getElement(); + const toolbarElt = this.toolbar.getElement(); if (this.accessibilityService.isScreenReaderOptimized()) { toolbarElt.style.display = 'block'; toolbarElt.ariaLabel = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat) ? localize('chat.codeBlock.toolbarVerbose', 'Toolbar for code block which can be reached via tab') : localize('chat.codeBlock.toolbar', 'Code block toolbar'); @@ -690,21 +694,10 @@ export class CodeCompareBlockPart extends Disposable { this.layout(width); this.diffEditor.updateOptions({ ariaLabel: localize('chat.compareCodeBlockLabel', "Code Edits") }); - this.toolbar1.clear(); - this.toolbar1.push(toAction({ - label: basename(data.edit.uri), - tooltip: localize('chat.edit.tooltip', "Open '{0}'", this.labelService.getUriLabel(data.edit.uri, { relative: true })), - run: () => { - this.openerService.open(data.edit.uri, { fromUserGesture: true, allowCommands: false }); - }, - id: '', - }), { icon: false, label: true }); - - if (data.hideToolbar) { - dom.hide(this.toolbar2.getElement()); - } else { - dom.show(this.toolbar2.getElement()); - } + this.resourceLabel.element.setFile(data.edit.uri, { + fileKind: FileKind.FILE, + fileDecorations: { colors: true, badges: false } + }); } reset() { @@ -732,10 +725,14 @@ 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, @@ -776,7 +773,7 @@ export class CodeCompareBlockPart extends Disposable { this._lastDiffEditorViewModel.value = undefined; } - this.toolbar2.context = { + this.toolbar.context = { edit: data.edit, element: data.element, diffEditor: this.diffEditor, @@ -888,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 ff0a4455486f8..c15a1f6aee3d6 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 4c3fa0ee29c5e..2d8bf253fd015 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 647ac002a13a4..8910e307eab93 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; } @@ -209,14 +210,14 @@ class AgentCompletions extends Disposable { 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.terminal' ? `0000` : ``; + 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 { @@ -236,9 +237,9 @@ 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: getFilterText(agent, c.name), commitCharacters: [' '], @@ -248,12 +249,28 @@ class AgentCompletions extends Disposable { kind: CompletionItemKind.Text, // The icons are disabled here anyway 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); @@ -297,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; } @@ -354,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(); @@ -362,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; } @@ -401,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 e58d0fd829436..24aafd6c10177 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,14 +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) { textDecorations.push({ range: agentPart.editorRange }); - if (agentSubcommandPart) { - textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); - } + } + if (agentSubcommandPart) { + textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); } if (slashCommandPart) { @@ -275,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 index 2ad7122eb9200..893353b52f166 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +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 } from 'vs/editor/contrib/hover/browser/hoverTypes'; +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'; @@ -15,6 +15,7 @@ import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/c 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 { @@ -49,22 +50,33 @@ export class ChatAgentHoverParticipant implements IEditorHoverParticipant { if (!hoverParts.length) { - return Disposable.None; + return new RenderedHoverParts([]); } - const store = new DisposableStore(); - const hover = store.add(this.instantiationService.createInstance(ChatAgentHover)); - store.add(hover.onDidChangeContents(() => context.onContentsChanged())); - const agent = hoverParts[0].agent; + 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); - context.fragment.appendChild(wrapper.domNode); + 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.'); - return store; } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index f827efc194fc6..aaf3f574c4817 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; @@ -518,6 +547,10 @@ flex-wrap: wrap; } +.interactive-session .interactive-item-container.interactive-request .chat-attached-context { + margin-top: -8px; +} + .interactive-session .chat-attached-context .chat-attached-context-attachment { padding: 2px; border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); @@ -526,6 +559,10 @@ max-width: 100%; } +.interactive-session .interactive-item-container.interactive-request .chat-attached-context .chat-attached-context-attachment { + padding-right: 6px; +} + .interactive-session-followups { display: flex; flex-direction: column; diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index 8a57732c95daf..449ff1bc2dd95 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 da0e5eba9d97a..08d7013d770b0 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 @@ -73,7 +73,6 @@ export interface IChatAgentData { isDynamic?: boolean; metadata: IChatAgentMetadata; slashCommands: IChatAgentCommand[]; - defaultImplicitVariables?: string[]; locations: ChatAgentLocation[]; } @@ -126,6 +125,7 @@ export interface IChatAgentRequest { enableCommandDetection?: boolean; variables: IChatRequestVariableData; location: ChatAgentLocation; + locationData?: IChatLocationData; acceptedConfirmationData?: any[]; rejectedConfirmationData?: any[]; } @@ -174,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) @@ -345,6 +346,16 @@ 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._agents.get(id); if (!data?.impl) { @@ -385,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 3c4cce05e0165..15451f0de58c6 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/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 1760a265c3baf..b7c992fe5aea0 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -80,7 +80,8 @@ export type IChatProgressRenderableResponseContent = Exclude; - asString(): string; + toMarkdown(): string; + toString(): string; } export interface IChatResponseModel { @@ -159,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; @@ -176,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); @@ -232,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); }); @@ -264,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(); } @@ -725,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, diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index 73276b9464dc1..e6492570295d6 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 a6921314d1896..645ba778d362e 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'; @@ -159,6 +160,7 @@ export interface IChatConfirmation { title: string; message: string; data: any; + buttons?: string[]; isUsed?: boolean; kind: 'confirmation'; } @@ -251,6 +253,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 +301,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 1ab8c2a4998ff..b18e59ac6b404 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, ChatRequestRemovalReason, 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); } @@ -569,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, @@ -594,6 +494,7 @@ export class ChatService extends Disposable implements IChatService { enableCommandDetection, attempt, location, + locationData: options?.locationData, acceptedConfirmationData: options?.acceptedConfirmationData, rejectedConfirmationData: options?.rejectedConfirmationData, }; @@ -611,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 = {}; @@ -670,13 +571,13 @@ export class ChatService extends Disposable implements IChatService { chatSessionId: model.sessionId, location }); - const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; + 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); } - completeResponseCreated(); - this.logService.error(`Error while handling chat request: ${toErrorMessage(err)}`); - 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 0000000000000..ec8d5aa339ecf --- /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 2d43f1c039682..3b19cd512b9b1 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 130ef76979b17..9d9962ed350a8 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, IChatRequestVariableEntry, 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'; @@ -68,6 +68,7 @@ export interface IChatRequestViewModel { readonly message: IParsedChatRequest | IChatFollowup; readonly messageText: string; readonly attempt: number; + readonly variables: IChatRequestVariableEntry[]; currentRenderedHeight: number | undefined; } @@ -78,6 +79,13 @@ export interface IChatResponseMarkdownRenderData { originalMarkdown: IMarkdownString; } +export interface IChatResponseMarkdownRenderData2 { + renderedWordCount: number; + lastRenderTime: number; + isFullyRendered: boolean; + originalMarkdown: IMarkdownString; +} + export interface IChatProgressMessageRenderData { progressMessage: IChatProgressMessage; @@ -101,13 +109,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; @@ -339,6 +362,10 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.attempt; } + get variables() { + return this._model.variableData.variables; + } + currentRenderedHeight: number | undefined; constructor( @@ -484,7 +511,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 +519,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 c3b7ff8822c53..b81d391186eae 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(/\[([^\]]+)\]\(([^)]+)\)|\p{sc=Han}|[^\s\|\-|\p{sc=Han}]+/gu)); + // 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 a857b64cb1df9..d5ed699cd9b6d 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 0000000000000..a1c70e6d1a48b --- /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 1276da1bd4248..b19a50e5e44d5 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -13,7 +13,6 @@ 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 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: string; + 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; } @@ -250,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 0000000000000..5cc34e72faff3 --- /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 c162f5859d7f9..ae70f2f56a12c 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']]); 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 1c0cdf3fe1937..58586530c701a 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -805,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; @@ -818,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); @@ -827,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_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap index 0d3458d76b7d4..9bfd3b945e811 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<details>3</details></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 3bb96899c111c..c0b5a277aac57 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<details id="id1" style="display: none">2<details id="my id 2">3</details></details>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_remote_images.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap index 34a719b061328..1241ef62b5faa 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 @@ -
<img src="http://disallowed.com/image.jpg">
\ 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 023b2e6a84676..5b482726d3a7b 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 0000000000000..89991e7676ea4 --- /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 0000000000000..d704b7b322dda --- /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 e9975f252834d..657ed1c961caf 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts @@ -27,6 +27,18 @@ 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'); md.supportHtml = true; 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 dcbe7a1782acb..2da25bc9817a2 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'; 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 383288306cb99..78fe7781692e7 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 cf9cc42dfc6a9..be8bb0fed9265 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 5847d813dfe49..b26d84334a081 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 index 21bd0b3930ffa..f14ffdaa65188 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts @@ -8,7 +8,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti 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 * as assert from 'assert'; +import assert from 'assert'; const testAgentId = 'testAgent'; const testAgentData: IChatAgentData = { 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 b9f38a04549b8..81efbdca4b699 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 1e9ad196f8b68..df32dfed2f4d8 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 314b4589e312e..665c2e386b191 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'], - ]; - - cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); - }); + 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)); + }); + + 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('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")'], - ]; + 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)); + 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)); + }); }); + }); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index 9bf3d63938fe6..ebdde0c0d2223 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -3,12 +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 { 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 { languageModelExtensionPoint, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; +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'; @@ -51,7 +53,7 @@ suite('LanguageModels', function () { maxInputTokens: 100, maxOutputTokens: 100, }, - provideChatResponse: async () => { + sendChatRequest: async () => { throw new Error(); }, provideTokenCount: async () => { @@ -70,7 +72,7 @@ suite('LanguageModels', function () { maxInputTokens: 100, maxOutputTokens: 100, }, - provideChatResponse: async () => { + sendChatRequest: async () => { throw new Error(); }, provideTokenCount: async () => { @@ -103,5 +105,56 @@ suite('LanguageModels', function () { 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 2ff31b0173c85..2ad2f91c6b1f8 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 249fd8e457c6b..85d4a9cdb2fa7 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 38fd5502ae2c8..17e6217a6a99c 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/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 052608edd82c2..15fba7682218a 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 b047830a88148..952a32eac79e2 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 bfbdc1d3f50e9..c739020796962 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/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 6fbc04ff214b9..e06219499e2c7 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 f5b2c84ebcb5b..5b5081623cf63 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 7cdd7d910ad4c..043e3f5c9e71e 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 f89603047934e..42cc4253ad20e 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 10b9058bfb9cb..fe3632fb8d070 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'; diff --git a/src/vs/workbench/contrib/comments/browser/commentColors.ts b/src/vs/workbench/contrib/comments/browser/commentColors.ts index 08d44a3224bca..b66b9590f763c 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 92b52ac5402b3..725b522f9561d 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 47e5285466c03..f7f6688cfbe28 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 9c9c8e24e5055..0a73f27023a44 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, @@ -121,7 +118,7 @@ export class CommentReply extends Disposable { this.expandReplyArea(); } else if (hasExistingComments) { this.createReplyButton(this.commentEditor, this.form); - } else if (focus && (this._commentThread.comments && this._commentThread.comments.length === 0)) { + } else if (focus && (!this._commentThread.comments || this._commentThread.comments.length === 0)) { this.expandReplyArea(); } this._error = dom.append(this.form, dom.$('.validation-error.hidden')); @@ -213,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) { @@ -368,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 14db2828ee2e3..7156f465fa58e 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 2849c9afd765f..8333654958e64 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(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index a919fe262a9d5..521455bb77a5a 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'; @@ -104,6 +104,7 @@ export class CommentThreadWidget extends 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/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index c55036a111dc3..09aeddd25e6a0 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 { @@ -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(); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 6caf357feb613..7f0dfde0e6414 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 938c658fd2dea..8527341bdae05 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/timestamp.ts b/src/vs/workbench/contrib/comments/browser/timestamp.ts index 47faa02f1c9ca..583ccaa7963d9 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 84e77afe298ca..9c7d9e80c1683 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 e903b24dc3987..3f607d926f39e 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/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 3546424023ce5..0515cc780f320 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 1b53d40047f18..d29256d907240 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 59a3a4dd4bfa1..11a82d72c3ac9 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 8b989fb46f485..ac8dc3ed3d0fc 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 80bdc5ffbd85e..6ee016a167c23 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 ab797aa530c18..b85da3db794d2 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."), @@ -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', diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index a4d5c4e74e258..50a1b351fc2b2 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 f7b758666b100..19b4728052475 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 7810995bb38b2..4f613027aaf69 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 4ab2ba9d777c7..5e3d5e9d583db 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); + } + + for (const { name, value } of variables) { + if (!values.has(name)) { + values.set(name, value); + } + } + } + } - 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}`); + 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 b534c3fba676a..3b2bf38563954 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 d09eada1d2abb..8c52537a0ae04 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 e2d54e9a29621..79c3cc8c1237f 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 add88e75098ba..7711f4ccc488d 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 1b920801decf9..7c9b48503a20c 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 fbe36b38c1a51..fc948f97b4a54 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 5baf2c48fb2c2..59696ad50041a 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 4f183f3701a80..c282aa3fab968 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 bae5c31696c7c..50d81a8f04127 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 4231608a29bba..54c881dfb53d5 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 5dc2a66b07388..7adb71aa31703 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 7369a36760712..22014a4ad96b7 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 e48607b9edab5..41f1a55b5570c 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 838140a91d09e..684209657352a 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 13f7d1d902dc3..f9c67ec895892 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 50eacfd65e25e..963e3d553e4df 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 acf1e4678177e..1b193bd9d5d3f 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 45d19aee6f95f..47baa50ee414c 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 4402ed3f3b5c9..6556849e5e90b 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 a5ba220e380c5..2f591514d5a2d 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', () => { @@ -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); + + assertVariable(session, scope, disposables, linkDetector, false); + + }); + + 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); - 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, []); - - 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:'); - - 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 6b47aa1dc3f18..292fa730381cc 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 f5ec3d56c5adb..e4473594e4ce5 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 c92a240a22055..41c662c091b8d 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,7 @@ * Licensed 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'; 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 71b53f1ab2522..6cde637373c15 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 c45e6dba417c5..480c811fc87f8 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 7647820f3d0bd..61e92664e502b 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 a4b59eee38de0..b58f2509a7b01 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 672ae3c306195..aad31249a2c5f 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 585497db087be..d6b033ff16f67 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 d42de786cb5c3..6e2f76448ec38 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 6a38dac9fdc43..1b888a6e147ec 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,7 @@ * Licensed 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'; 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 3f3549c604e21..5a9f3f2f415d0 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 f385cbbb7b3de..54da8d61f1983 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 0000000000000..5eb511bc678d0 --- /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 dabf666859cd7..92b54afe7caa6 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 0000000000000..220e79d6a8485 --- /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 2246b7fa2486b..cfcc1af8b877a 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 26c5549841b06..e3cf88f9b07f8 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 4d2d38f1f651e..e618a4552da03 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 9247a0ee6364a..89bb6968f5e9c 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 efdf2f940c3d4..bd81c9c13e238 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 c6be96b073da1..244f3dfe5bcab 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 f3a0b28bf0439..76d14f29f5493 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 9969928a2b528..48580f61c8229 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 1ee623c22a0f5..a298cf3c232c3 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 dc3c89dcdc803..d3109d7f7d95f 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 42680a4ba1f02..794127b7f6a67 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 4152bf2f35130..1f510c15b173b 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 0ff4dca2f7189..697fafb73d899 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, IExtensionArg } 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'; @@ -81,6 +78,7 @@ import { CONTEXT_KEYBINDINGS_EDITOR } from 'vs/workbench/contrib/preferences/com 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 */); @@ -128,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."), @@ -380,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); @@ -635,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, - }); - - 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')), - menu: [{ - id: autoUpdateExtensionsSubMenu, - order: 1, - }], - 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') - }); - + const enableAutoUpdateWhenCondition = ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false); this.registerExtensionAction({ - id: 'configureExtensionsAutoUpdate.selected', - title: localize('configureExtensionsAutoUpdate.selected', "Selected Extensions"), - toggled: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlySelectedExtensions'), + id: 'workbench.extensions.action.enableAutoUpdate', + title: localize2('enableAutoUpdate', 'Enable Auto Update for All Extensions'), + category: ExtensionsLocalizedLabel, + precondition: enableAutoUpdateWhenCondition, menu: [{ - id: autoUpdateExtensionsSubMenu, - order: 2, + id: MenuId.ViewContainerTitle, + order: 5, + group: '1_updates', + when: enableAutoUpdateWhenCondition + }, { + id: MenuId.CommandPalette, }], - run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, 'onlySelectedExtensions') + run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateValue(true) }); + const disableAutoUpdateWhenCondition = ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, false); this.registerExtensionAction({ - id: 'configureExtensionsAutoUpdate.none', - title: localize('configureExtensionsAutoUpdate.none', "None"), - toggled: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false), + id: 'workbench.extensions.action.disableAutoUpdate', + title: localize2('disableAutoUpdate', 'Disable Auto Update for All Extensions'), + precondition: disableAutoUpdateWhenCondition, + category: ExtensionsLocalizedLabel, menu: [{ - id: autoUpdateExtensionsSubMenu, - order: 3, + id: MenuId.ViewContainerTitle, + order: 5, + group: '1_updates', + when: disableAutoUpdateWhenCondition + }, { + id: MenuId.CommandPalette, }], - run: (accessor: ServicesAccessor) => accessor.get(IConfigurationService).updateValue(AutoUpdateConfigurationKey, false) + run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateValue(false) }); this.registerExtensionAction({ @@ -724,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'), @@ -854,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."), + [] + ); + } } }); @@ -1364,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(); } @@ -1386,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); @@ -1467,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'), @@ -1550,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) => { @@ -1756,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 fc174208ee612..3f7e240ced1c4 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, IExtensionArg } 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; - } - - let clazz = (this.action || this.extensionActions[0])?.class || ''; - clazz = clazz ? `${clazz} action-dropdown` : 'action-dropdown'; - if (this._menuActions.length === 0) { - clazz += ' action-dropdown'; + 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; } - 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; @@ -1291,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, @@ -1348,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; @@ -1380,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 }; @@ -1392,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; @@ -1540,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) @@ -1554,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) ]]); @@ -1570,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; @@ -1684,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, @@ -1735,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, @@ -1785,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, @@ -1836,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, @@ -1872,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, @@ -2184,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; @@ -2284,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, @@ -2723,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); } @@ -3015,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 683098f05b6e6..95c05e762e5d3 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 7ca65d956cee6..9a81262ecf8f7 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 4dcb55bb040ba..933788b162f06 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 3d725460a201a..4b53d91cbc5a6 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 f6b528933c646..78abd2bffaba4 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; } @@ -721,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; }); } @@ -757,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); } } @@ -950,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()]); @@ -969,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()) { @@ -991,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); } @@ -1012,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() { @@ -1218,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; } @@ -1263,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[] = []; @@ -1304,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 }); } } } @@ -1547,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; @@ -1618,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, } }); } @@ -1640,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); } @@ -1661,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(); @@ -1702,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; @@ -1738,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; + private shouldAutoUpdateExtension(extension: IExtension): boolean { + if (extension.deprecationInfo?.disallowInstall) { + return false; + } + + const autoUpdateValue = this.getAutoUpdateValue(); + + if (autoUpdateValue === false) { + const extensionsToAutoUpdate = this.getEnabledAutoUpdateExtensions(); + const extensionId = extension.identifier.id.toLowerCase(); + if (extensionsToAutoUpdate.includes(extensionId)) { + return true; } - const shouldBePinned = !this.shouldAutoUpdateExtension(e); - if (e.local && e.local.pinned !== shouldBePinned) { - await this.extensionManagementService.updateMetadata(e.local, { pinned: shouldBePinned }); + if (this.isAutoUpdateEnabledForPublisher(extension.publisher) && !extensionsToAutoUpdate.includes(`-${extensionId}`)) { + return true; } - })); - } + return false; + } - private shouldAutoUpdateExtension(extension: IExtension): boolean { - const autoUpdate = this.getAutoUpdateValue(); - if (isBoolean(autoUpdate)) { - return autoUpdate; + if (extension.pinned) { + return false; } - if (autoUpdate === 'onlyEnabledExtensions') { + 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 { @@ -1771,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 { @@ -1789,101 +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'); + const disabledAutoUpdateExtensions = this.getDisabledAutoUpdateExtensions(); + const extensionId = extensionOrPublisher.identifier.id.toLowerCase(); + const extensionIndex = disabledAutoUpdateExtensions.indexOf(extensionId); + if (enable) { + if (extensionIndex !== -1) { + disabledAutoUpdateExtensions.splice(extensionIndex, 1); + } } - 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 { + if (extensionIndex === -1) { + disabledAutoUpdateExtensions.push(extensionId); } } - } else { - const extensionId = extensionOrPublisher.identifier.id.toLowerCase(); - const enableAutoUpdatesForPublisher = this.isAutoUpdateEnabledFor(extensionOrPublisher.publisher.toLowerCase()); - const enableAutoUpdatesForExtension = autoUpdateExtensions.includes(extensionId); - const disableAutoUpdatesForExtension = autoUpdateExtensions.includes(`-${extensionId}`); + this.setDisabledAutoUpdateExtensions(disabledAutoUpdateExtensions); + if (enable && extensionOrPublisher.pinned) { + await this.extensionManagementService.updateMetadata(extensionOrPublisher.local, { pinned: false }); + } + this._onChange.fire(extensionOrPublisher); + } - if (enable) { - if (disableAutoUpdatesForExtension) { - autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(`-${extensionId}`), 1); - update = true; + else { + const enabledAutoUpdateExtensions = this.getEnabledAutoUpdateExtensions(); + if (isString(extensionOrPublisher)) { + if (EXTENSION_IDENTIFIER_REGEX.test(extensionOrPublisher)) { + throw new Error('Expected publisher string, found extension identifier'); } - if (enableAutoUpdatesForPublisher) { - if (enableAutoUpdatesForExtension) { - autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(extensionId), 1); - update = true; - } - } else { - if (!enableAutoUpdatesForExtension) { - autoUpdateExtensions.push(extensionId); - update = true; + extensionOrPublisher = extensionOrPublisher.toLowerCase(); + if (this.isAutoUpdateEnabledFor(extensionOrPublisher) !== enable) { + if (enable) { + enabledAutoUpdateExtensions.push(extensionOrPublisher); + } else { + if (enabledAutoUpdateExtensions.includes(extensionOrPublisher)) { + enabledAutoUpdateExtensions.splice(enabledAutoUpdateExtensions.indexOf(extensionOrPublisher), 1); + } } } - if (extensionOrPublisher.local?.pinned) { - await this.extensionManagementService.updateMetadata(extensionOrPublisher.local, { pinned: false }); + this.setEnabledAutoUpdateExtensions(enabledAutoUpdateExtensions); + for (const e of this.installed) { + if (e.publisher.toLowerCase() === extensionOrPublisher) { + this._onChange.fire(e); + } } - } - // Disable Auto Updates - else { - if (enableAutoUpdatesForExtension) { - autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(extensionId), 1); - update = true; + } 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) { + enabledAutoUpdateExtensions.splice(enabledAutoUpdateExtensions.indexOf(`-${extensionId}`), 1); + } + if (enableAutoUpdatesForPublisher) { + if (enableAutoUpdatesForExtension) { + enabledAutoUpdateExtensions.splice(enabledAutoUpdateExtensions.indexOf(extensionId), 1); + } + } else { + if (!enableAutoUpdatesForExtension) { + enabledAutoUpdateExtensions.push(extensionId); + } + } } - if (enableAutoUpdatesForPublisher) { - if (!disableAutoUpdatesForExtension) { - autoUpdateExtensions.push(`-${extensionId}`); - update = true; + // Disable Auto Updates + else { + if (enableAutoUpdatesForExtension) { + enabledAutoUpdateExtensions.splice(enabledAutoUpdateExtensions.indexOf(extensionId), 1); } - } else { - if (disableAutoUpdatesForExtension) { - autoUpdateExtensions.splice(autoUpdateExtensions.indexOf(`-${extensionId}`), 1); - update = true; + 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); + } + } } } @@ -1952,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.")); @@ -2024,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) { @@ -2511,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; } @@ -2524,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 7bca424370314..ed8c3395cccd1 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 a59e7c53d77ed..1de72e81ee9be 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -485,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 d77881dfa3920..9179fa761d368 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 179624e281b9c..c619e3a0b5747 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'; 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 a743e9e184ea0..588f6a3cec50e 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 8c96cf7a48ead..efdcd3abd9869 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 4e62c45838cf4..be1ca52ad70e7 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 4194641f2ba17..197f605903121 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'; @@ -177,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); }); }); }); @@ -206,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); @@ -1733,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 () => { @@ -1759,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, 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 () => { @@ -1792,12 +1792,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, 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); @@ -1826,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 () => { @@ -1848,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 () => { @@ -1870,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 () => { @@ -2111,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 () => { @@ -2164,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 () => { @@ -2185,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 () => { @@ -2211,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, 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 () => { @@ -2244,12 +2244,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, 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); @@ -2278,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 () => { @@ -2502,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 () => { @@ -2652,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 c5de87fd0d314..ef60bf9660553 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'; @@ -505,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]; @@ -532,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]; @@ -574,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 475cc3e3e03f3..eee8b207a292d 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'; @@ -57,6 +57,7 @@ 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', () => { @@ -111,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({ @@ -1431,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 () => { @@ -1453,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 () => { @@ -1489,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); @@ -1546,15 +1556,17 @@ 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 }); @@ -1565,101 +1577,41 @@ 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(), []); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), ['pub.a']); - test('Test enable auto update for publisher when auto update mode is onlySelectedExtensions', async () => { - stubConfiguration('onlySelectedExtensions'); + await testObject.updateAutoUpdateValue(false); - 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']); + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); }); - 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; + test('Test reset autoupdate extensions state when auto update is enabled', async () => { + stubConfiguration(false); + instantiationService.stub(IDialogService, { + confirm: () => Promise.resolve({ confirmed: true }) }); - 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) => { + 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].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']); - }); + await testObject.updateAutoUpdateEnablementFor(testObject.local[0], true); - 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'); + assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), ['pub.a']); + assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); - 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.updateAutoUpdateValue(true); - 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 { @@ -1673,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; + }, + }); } }); } @@ -1752,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 6ca5f7d48f06f..3388ec7baeab4 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 842599bb6be8c..5be58d5736596 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/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 963e5c5692506..b910fb3f2eef2 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/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 81bf68c3e0b4c..97fb6ec7c65f3 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 c38f9987d22e0..555cff9f3d257 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 35a5a532e0ff4..d525ba5860c80 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': [ diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index a94048551aa3f..940bfb827920b 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -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/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index a5de264bfcfdc..f7560f5a89f7a 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._register(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/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index f104ba762eb04..75b57831d842b 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 2c08f73b54c00..66481ab3a2383 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 938fed848a2de..13870699b3ccc 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 cea1d9c71724f..ea30440fa54e6 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 e1e8db24108be..f5070edb1791d 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 ef6adefcdd21b..1675b2d45714a 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 ca865ea076ce4..94f9a716751e2 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 75e6c3b5cdc8b..f3d6c8fb19885 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 4ab19a40890ca..7d3e40412716f 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/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts index 91a719e19996f..797dae2825daf 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,7 +37,7 @@ 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(); }, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index b7c33b6df158c..2009001a7ea25 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_AGENT, 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); @@ -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 }); @@ -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) + ) + ), + }] }); } @@ -387,6 +391,11 @@ export class ConfigureInlineChatAction extends AbstractInlineChatAction { icon: Codicon.settingsGear, precondition: CTX_INLINE_CHAT_VISIBLE, f1: true, + menu: { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: 'zzz', + order: 5 + } }); } @@ -478,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, } }); } @@ -490,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 c305b01aab416..92ba84eb48e70 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -11,9 +11,9 @@ 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 { 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'; @@ -22,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 { @@ -31,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; @@ -45,10 +49,11 @@ export class InlineChatContentWidget implements IContentWidget { private readonly _widget: ChatWidget; constructor( - location: ChatAgentLocation, + 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)); @@ -68,12 +73,13 @@ export class InlineChatContentWidget implements IContentWidget { { 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(); } })); @@ -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); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 7b0c50ff990a1..782bc3b95bc12 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,28 +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_RESPONSE_TYPES, 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, 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 { 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', @@ -82,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' @@ -106,14 +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 _ctxResponseType: IContextKey; private readonly _ctxUserDidEdit: IContextKey; + private readonly _ctxRequestInProgress: IContextKey; private _messages = this._store.add(new Emitter()); @@ -144,28 +142,42 @@ 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._ctxUserDidEdit = CTX_INLINE_CHAT_USER_DID_EDIT.bindTo(contextKeyService); - this._ctxResponseTypes = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(contextKeyService); + this._ctxResponseType = CTX_INLINE_CHAT_RESPONSE_TYPE.bindTo(contextKeyService); + this._ctxRequestInProgress = CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); this._ui = new Lazy(() => { - let location = ChatAgentLocation.Editor; + + 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 + // 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 = ChatAgentLocation.Notebook; + 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 }; @@ -304,7 +316,6 @@ export class InlineChatController implements IEditorContribution { if (m === Message.ACCEPT_INPUT) { // user accepted the input before having a session options.autoSend = true; - this._ui.value.zone.widget.updateProgress(true); this._ui.value.zone.widget.updateInfo(localize('welcome.2', "Getting ready...")); } else { createSessionCts.cancel(); @@ -386,6 +397,7 @@ export class InlineChatController implements IEditorContribution { this._updatePlaceholder(); 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 @@ -424,120 +436,14 @@ export class InlineChatController implements IEditorContribution { })); this._sessionStore.add(this._session.chatModel.onDidChange(async e => { - if (e.kind === 'addRequest' && e.request.response) { - this._ui.value.zone.widget.updateProgress(true); - - const listener = e.request.response.onDidChange(() => { - - if (e.request.response?.isCanceled || e.request.response?.isComplete) { - this._ui.value.zone.widget.updateProgress(false); - listener.dispose(); - } - }); - } else if (e.kind === 'removeRequest') { + 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 - const exchange = this._session!.exchanges.find(candidate => candidate.prompt.request.id === e.requestId); - if (exchange && this._editor.hasModel()) { - // undo till this point - this._session!.hunkData.ignoreTextModelNChanges = true; - try { - - const model = this._editor.getModel(); - const targetAltVersion = exchange.prompt.modelAltVersionId; - while (targetAltVersion < model.getAlternativeVersionId() && model.canUndo()) { - await model.undo(); - } - } finally { - this._session!.hunkData.ignoreTextModelNChanges = 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.agent.slashCommands) { - return undefined; - } - const widget = this._chatWidgetService.getWidgetByInputUri(model.uri); - if (widget !== this._ui.value.zone.widget.chatWidget && widget !== this._ui.value.content.chatWidget) { - return undefined; - } - - const result: CompletionList = { suggestions: [], incomplete: false }; - for (const command of this._session.agent.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?.agent.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; - } + await this._session!.undoChangesUntil(e.requestId); } - collection.set(newDecorations); - }; - const inputInputEditor = this._ui.value.content.chatWidget.inputEditor; - const zoneInputEditor = this._ui.value.zone.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; @@ -609,9 +515,6 @@ export class InlineChatController implements IEditorContribution { return State.WAIT_FOR_INPUT; } - const input = request.message.text; - this._ui.value.zone.widget.value = input; - this._session.addInput(new SessionPrompt(request, this._editor.getModel()!.getAlternativeVersionId())); return State.SHOW_REQUEST; @@ -622,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); @@ -629,7 +534,7 @@ export class InlineChatController implements IEditorContribution { assertType(request.response); this._showWidget(false); - this._ui.value.zone.widget.value = request.message.text; + // this._ui.value.zone.widget.value = request.message.text; this._ui.value.zone.widget.selectAll(false); this._ui.value.zone.widget.updateInfo(''); @@ -686,6 +591,8 @@ export class InlineChatController implements IEditorContribution { // apply edits const handleResponse = () => { + this._updateCtxResponseType(); + if (!localEditGroup) { localEditGroup = response.response.value.find(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); } @@ -743,16 +650,19 @@ export class InlineChatController implements IEditorContribution { 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(editState); - - this._ui.value.zone.widget.updateToolbar(true); - this._ui.value.zone.widget.updateProgress(false); + this._ctxRequestInProgress.set(false); return next; } @@ -763,20 +673,6 @@ 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); let newPosition: Position | undefined; @@ -796,9 +692,20 @@ export class InlineChatController implements IEditorContribution { } else if (response instanceof ReplyResponse) { // real response -> complex... this._ui.value.zone.widget.updateStatus(''); - this._ui.value.zone.widget.updateToolbar(true); - 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); @@ -842,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(); @@ -871,6 +778,7 @@ 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) { @@ -888,7 +796,7 @@ export class InlineChatController implements IEditorContribution { 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); } @@ -898,13 +806,6 @@ export class InlineChatController implements IEditorContribution { } 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._ui.value.content.show(widgetPosition); } else { @@ -915,11 +816,6 @@ export class InlineChatController implements IEditorContribution { } } - if (this._session && this._ui.rawValue?.zone) { - this._ui.rawValue?.zone.updateBackgroundColor(widgetPosition, this._session.wholeRange.value); - } - - this._ctxVisible.set(true); return widgetPosition; } @@ -937,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); @@ -958,7 +879,6 @@ 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 { @@ -1065,12 +985,13 @@ export class InlineChatController implements IEditorContribution { } 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', @@ -1090,13 +1011,13 @@ export class InlineChatController implements IEditorContribution { } async cancelSession() { - - 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', @@ -1147,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 eca335cb37533..0000000000000 --- 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 78acedb3c2723..4d18fe88fce63 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -33,6 +33,7 @@ import { ILogService } from 'vs/platform/log/common/log'; 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 = { @@ -88,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[] = []; @@ -138,7 +130,7 @@ 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; @@ -155,7 +147,7 @@ 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, @@ -199,17 +191,35 @@ export class Session { 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 exchanges(): readonly SessionExchange[] { - return this._exchange; + get lastExchange(): SessionExchange | undefined { + return this._exchanges[this._exchanges.length - 1]; } - get lastExchange(): SessionExchange | undefined { - return this._exchange[this._exchange.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 { @@ -259,7 +269,7 @@ export class Session { 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.chatResponse }); @@ -555,9 +565,9 @@ export class HunkData { this._textModel0.pushEditOperations(null, edits, () => null); } - async recompute(editState: IChatTextEditGroupState) { + 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); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index e7c39a315ef3d..44aecd6d917d0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -10,7 +10,7 @@ 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'; @@ -27,8 +27,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic 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'; @@ -46,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 { @@ -92,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(); @@ -383,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_AGENT.bindTo(contextKeyService); - chatAgentService.onDidChangeAgents(() => { + 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 c98fcfa30f8d0..2e2f692cc88fd 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'; @@ -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 f714914526682..e920c34947bef 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,7 +522,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { private readonly _accessibleViewer = this._store.add(new MutableDisposable()); constructor( - location: ChatAgentLocation, + location: IChatWidgetLocationOptions, private readonly _parentEditor: ICodeEditor, options: IInlineChatWidgetConstructionOptions, @IContextKeyService contextKeyService: IContextKeyService, @@ -608,7 +535,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @IChatService chatService: IChatService, @IHoverService hoverService: IHoverService, ) { - super(location, { ...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 @@ -617,7 +544,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { let result = super.contentHeight; if (this._accessibleViewer.value) { - result += this._accessibleViewer.value.height; + result += this._accessibleViewer.value.height + 8 /* padding */; } return result; @@ -629,7 +556,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { 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 88c16b8070da0..adc3b413558d5 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 { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IChatWidgetLocationOptions } from 'vs/workbench/contrib/chat/browser/chatWidget'; export class InlineChatZoneWidget extends ZoneWidget { @@ -29,12 +29,12 @@ export class InlineChatZoneWidget extends ZoneWidget { private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; private _dimension?: Dimension; - private _indentationWidth: number | undefined; constructor( - location: ChatAgentLocation, + location: IChatWidgetLocationOptions, editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, + @ILogService private _logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, ) { @@ -47,9 +47,6 @@ export class InlineChatZoneWidget extends ZoneWidget { })); this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { - telemetrySource: 'interactiveEditorWidget-toolbar', - inputMenuId: MenuId.ChatExecute, - widgetMenuId: MENU_INLINE_CHAT_WIDGET, statusMenuId: { menu: MENU_INLINE_CHAT_WIDGET_STATUS, options: { @@ -64,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._computeHeight().linesValue); + 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 => { @@ -110,16 +126,14 @@ 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)); - 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)); + 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 _computeHeight(): { linesValue: number; pixelsValue: number } { @@ -147,86 +161,67 @@ export class InlineChatZoneWidget extends ZoneWidget { const height = this._computeHeight(); super.show(position, height.linesValue); - this._setWidgetMargins(position); this.widget.chatWidget.setVisible(true); this.widget.focus(); scrollState.restore(this.editor); - if (position.lineNumber > 1) { - this.editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position.delta(-1)), ScrollType.Immediate); - } else { - // reveal top of zone widget - const lineTop = this.editor.getTopForLineNumber(position.lineNumber); - const zoneTop = lineTop - height.pixelsValue; - const spaceBelowLine = this.editor.getScrollHeight() - this.editor.getBottomForLineNumber(position.lineNumber); - const minTop = this.editor.getScrollTop() - spaceBelowLine; - const newTop = Math.max(zoneTop, minTop); - - if (newTop < this.editor.getScrollTop()) { - this.editor.setScrollTop(newTop, ScrollType.Immediate); - } - } + this._revealTopOfZoneWidget(position, height); } override updatePositionAndHeight(position: Position): void { - super.updatePositionAndHeight(position, this._computeHeight().linesValue); - this._setWidgetMargins(position); - } + const scrollState = StableEditorBottomScrollState.capture(this.editor); + const height = this._computeHeight(); + super.updatePositionAndHeight(position, height.linesValue); + scrollState.restore(this.editor); - protected override _getWidth(info: EditorLayoutInfo): number { - return info.width - info.minimap.minimapWidth; + this._revealTopOfZoneWidget(position, height); } - 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 _revealTopOfZoneWidget(position: Position, height: { linesValue: number; pixelsValue: number }) { - private _calculateIndentationWidth(position: Position): number { - const viewModel = this.editor._getViewModel(); - if (!viewModel) { - return 0; - } + // reveal top of zone widget + + const lineNumber = position.lineNumber <= 1 ? 1 : 1 + position.lineNumber; - 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; + 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; } - 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; - } + if (newScrollTop < scrollTop || forceScrollTop) { + this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); + this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); } + } - return Math.max(0, this.editor.getOffsetForColumn(indentationLineNumber, indentationLevel)); // double-guard against invalie getOffsetForColumn-calls + protected override revealRange(range: Range, isLastLine: boolean): void { + // noop } - 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`; + protected override _getWidth(info: EditorLayoutInfo): number { + return info.width - info.minimap.minimapWidth; } 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 a2d1666ef79fc..a6b855ff0461c 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,36 +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-request { +.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; } -/* progress bit */ - -.monaco-workbench .inline-chat .progress { - position: relative; +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-request { + border: none; } -/* UGLY - fighting against workbench styles */ -.monaco-workbench .part.editor > .content .inline-chat .progress .monaco-progress-container { - top: 0; +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.minimal > .header { + top: 5px; + right: 10px; } + /* status */ -.monaco-workbench .inline-chat .status { +.monaco-workbench .inline-chat > .status { display: flex; justify-content: space-between; align-items: center; - margin-top: 3px; /*makes space for action focus borders: https://github.com/microsoft/vscode-copilot/issues/5814 */ + padding: 4px 6px 0 6px } .monaco-workbench .inline-chat .status .actions.hidden { @@ -78,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 { @@ -104,51 +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; -} + display: flex; + height: 18px; -.monaco-workbench .inline-chat .followUps.hidden { - display: none; -} + .actions-container { + gap: 3px + } -.monaco-workbench .inline-chat .chatMessage { - padding: 0 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 .chatMessage .chatMessageContent { - padding: 2px 2px; +.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 .chatMessage.hidden { - display: none; +.monaco-workbench .inline-chat .status { + .actions.text-style { + display: none; + } + .actions.button-style { + display: inherit; + } } -.monaco-workbench .inline-chat .status .actions { - display: flex; - padding-top: 3px; +.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 { @@ -166,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? */ @@ -186,45 +200,17 @@ background-color: var(--vscode-button-hoverBackground); } -/* preview */ - -.monaco-workbench .inline-chat .preview { - 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; -} +/* accessible diff viewer */ -.monaco-workbench .inline-chat .previewCreateTitle { - padding-top: 6px; +.monaco-workbench .inline-chat .diff-review { + padding: 4px 6px; + background-color: unset; } -.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 { +.monaco-workbench .inline-chat .diff-review.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 { @@ -248,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 829b3d78fc3d6..7da5cc3e97ef3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css @@ -5,7 +5,7 @@ .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); @@ -24,19 +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 .message > .codicon { - padding-right: 5px; - font-size: 12px; - line-height: 18px; +.monaco-workbench .inline-chat-content-widget.interactive-session .interactive-list { + display: none; } -.monaco-workbench .inline-chat-content-widget .hidden { +.monaco-workbench .inline-chat-content-widget.interactive-session .toolbar { display: none; + padding-top: 4px; } diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index a4ba41a17b2b3..b4c754b17590a 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -6,95 +6,24 @@ 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'; - - -export const enum InlineChatResponseTypes { - Empty = 'empty', - OnlyEdits = 'onlyEdits', - OnlyMessages = 'onlyMessages', - Mixed = 'mixed' -} - -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_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_RESPONSE_TYPES = new RawContextKey('inlineChatResponseTypes', InlineChatResponseTypes.Empty, localize('inlineChatResponseTypes', "What type was the responses have been receieved")); -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); - -// --- (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({ @@ -136,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 0088ab028f832..cc4ddad6e7ad6 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { DeferredPromise, raceCancellation, timeout } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -31,7 +31,7 @@ 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, IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +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'; @@ -42,7 +42,7 @@ import { IInlineChatSessionService } from '../../browser/inlineChatSessionServic 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'; @@ -156,6 +156,11 @@ suite('InteractiveChatController', function () { [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)], @@ -323,6 +328,7 @@ suite('InteractiveChatController', function () { assert.ok(session); assert.deepStrictEqual(session.wholeRange.value, new Range(3, 1, 3, 3)); // initial + ctrl.chatWidget.setInput('GENGEN'); ctrl.acceptInput(); assert.strictEqual(await ctrl.awaitStates([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]), undefined); @@ -540,6 +546,7 @@ suite('InteractiveChatController', function () { // 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); @@ -623,6 +630,7 @@ suite('InteractiveChatController', function () { // 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); @@ -770,4 +778,47 @@ suite('InteractiveChatController', function () { 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 86a3b614ff278..26e72175e2cd7 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'; 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 e8d229d81fcf3..c6724b095e49d 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 cc21b0371a639..2109f7ef7052d 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 46a52d9b844d0..20ce01d8e3469 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 ba994f540b6ef..a6b48582111b2 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 0000000000000..6dc4644b71ad2 --- /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.ts b/src/vs/workbench/contrib/issue/browser/issue.ts index 79e44855c34f7..4f097ba4603ac 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.ts @@ -240,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(); @@ -790,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'); @@ -800,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') { @@ -1076,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); @@ -1097,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(); } @@ -1202,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 index ca24026d161ca..1ac7d114454bd 100644 --- a/src/vs/workbench/contrib/issue/browser/issueFormService.ts +++ b/src/vs/workbench/contrib/issue/browser/issueFormService.ts @@ -40,11 +40,10 @@ export class IssueFormService implements IIssueMainService { // listen for messages from the main window mainWindow.addEventListener('message', async (event) => { if (event.data && event.data.sendChannel === 'vscode:triggerReporterMenu') { - // creates menu from contributed - const menu = this.menuService.createMenu(MenuId.IssueReporter, this.contextKeyService); + // gets menu actions 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]); + // render menu for (const action of actions) { try { if (action.item && 'source' in action.item && action.item.source?.id === event.data.extensionId) { @@ -61,8 +60,6 @@ export class IssueFormService implements IIssueMainService { const replyChannel = `vscode:triggerReporterMenuResponse`; mainWindow.postMessage({ replyChannel }, '*'); } - - menu.dispose(); } }); diff --git a/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts b/src/vs/workbench/contrib/issue/browser/issueQuickAccess.ts index 37d52199b5bed..a131f19d86584 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 { + 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(); diff --git a/src/vs/workbench/contrib/issue/issue/testReporterModel.test.ts b/src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts similarity index 98% rename from src/vs/workbench/contrib/issue/issue/testReporterModel.test.ts rename to src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts index d86022dec6a81..f90b10bade8d5 100644 --- a/src/vs/workbench/contrib/issue/issue/testReporterModel.test.ts +++ b/src/vs/workbench/contrib/issue/browser/test/testReporterModel.test.ts @@ -3,7 +3,9 @@ * 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/workbench/contrib/issue/browser/issueReporterModel'; import { IssueType } from 'vs/platform/issue/common/issue'; diff --git a/src/vs/workbench/contrib/issue/common/issue.ts b/src/vs/workbench/contrib/issue/common/issue.ts index 3ecd103cf589d..5834ba051be20 100644 --- a/src/vs/workbench/contrib/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 3b86046d12f43..9eacc2f757425 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 { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/common/issue'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; 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,11 +13,7 @@ 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'; @@ -27,7 +22,6 @@ 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 class NativeIssueContribution extends BaseIssueContribution { @@ -87,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/contrib/issue/electron-sandbox/issueMainService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueMainService.ts index a3cb28473af01..cf16313519ae5 100644 --- a/src/vs/workbench/contrib/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/workbench/contrib/issue/electron-sandbox/issueReporter.js b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js index cad5ddba090d4..15a7f2a211ddf 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporter.js @@ -4,8 +4,13 @@ *--------------------------------------------------------------------------------------------*/ //@ts-check +'use strict'; + (function () { - 'use strict'; + + /** + * @import { ISandboxConfiguration } from '../../../../base/parts/sandbox/common/sandboxTypes' + */ const bootstrapWindow = bootstrapWindowLib(); @@ -24,12 +29,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/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts index 05f002de5320e..ca9253bb1e94e 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterMain.ts @@ -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/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/workbench/contrib/issue/electron-sandbox/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts index 961ba3291a1fa..4fdf80be67df4 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts @@ -19,7 +19,7 @@ import { URI } from 'vs/base/common/uri'; 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); }); } @@ -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/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts index 6f6bdd1c1f488..02b3adb817d6b 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService2.ts @@ -12,7 +12,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; 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'; @@ -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 () => { @@ -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/contrib/issue/electron-sandbox/issueService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts index 3b5c8d26ed6c7..651dfa6c509c1 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueService.ts @@ -4,20 +4,17 @@ *--------------------------------------------------------------------------------------------*/ 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/contrib/issue/common/issue'; @@ -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/workbench/contrib/issue/electron-sandbox/process.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/process.contribution.ts new file mode 100644 index 0000000000000..43fe8fb39d2fe --- /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 0000000000000..60ebd4f898f2b --- /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 3feb89c66166f..daf83ca4cb097 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); } diff --git a/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css b/src/vs/workbench/contrib/languageStatus/browser/media/languageStatus.css index 4354ad022df87..25433bee4a4e0 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 c768ab602f033..098357445d007 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 b603ed444e619..a93fc9f990105 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 b96ba30f68c7d..a5c4206346387 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 9b18b5cd02829..b1c6b962fab53 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 095a297856490..fe1c3c04e266d 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 55e38f3d018d4..1889507c97429 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/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 40c99ee34579b..1a3dec090cccd 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 b8334c948d6e0..bec693957c3e2 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 b043f0840a13a..279baa80c146c 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 cc6e57008128c..f8d1d8b6fcdae 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 b751b6bc0ade2..7d564cb464666 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 29af08fbafedb..b75ca359a2dc9 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 85c475e3c2fe9..135d0d9a7ee4f 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 0441e4483d36e..a07bc71e67580 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 5b1ab36ce1055..30e6c69a64122 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/actions.ts @@ -10,13 +10,11 @@ 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 { getCommandsContext, resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { IEditorCommandsContext } from 'vs/workbench/common/editor'; -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 { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class GoToFileAction extends Action2 { @@ -40,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, + }); } } @@ -75,9 +80,15 @@ export class CollapseAllAction extends Action2 { }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - const { editor } = resolveCommandsContext(accessor.get(IEditorGroupsService), getCommandsContext(accessor, resourceOrContext, context)); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const resolvedContext = resolveCommandsContext(accessor, args); + const groupContext = resolvedContext.groupedEditors[0]; + if (!groupContext) { + return; + } + + const editor = groupContext.editors[0]; if (editor instanceof MultiDiffEditorInput) { const viewModel = await editor.getViewModel(); viewModel.collapseAll(); @@ -102,9 +113,15 @@ export class ExpandAllAction extends Action2 { }); } - async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { - const { editor } = resolveCommandsContext(accessor.get(IEditorGroupsService), getCommandsContext(accessor, resourceOrContext, context)); + async run(accessor: ServicesAccessor, ...args: unknown[]): Promise { + const resolvedContext = resolveCommandsContext(accessor, args); + + 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 ffe7a8738f523..2b5168dea9bfb 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 43f2f3eb899c4..44c3da5801d76 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 a53d4d6c3461e..43ff34c3ef565 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 ab728db53f279..721b711f5d2f0 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/editorHint/emptyCellEditorHint.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts index eeef10e64d2af..205bce806e1a3 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/find/findFilters.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts index 0901d295edd06..cf98120913cdb 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts @@ -3,16 +3,16 @@ * 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 { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { INotebookFindScope, NotebookFindScopeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export interface INotebookFindChangeEvent { markupInput?: boolean; markupPreview?: boolean; codeInput?: boolean; codeOutput?: boolean; - searchInRanges?: boolean; + findScope?: boolean; } export class NotebookFindFilters extends Disposable { @@ -70,31 +70,19 @@ export class NotebookFindFilters extends Disposable { } } - private _searchInRanges: boolean = false; + private _findScope: INotebookFindScope = { findScopeType: NotebookFindScopeType.None }; - get searchInRanges(): boolean { - return this._searchInRanges; + get findScope(): INotebookFindScope { + return this._findScope; } - set searchInRanges(value: boolean) { - if (this._searchInRanges !== value) { - this._searchInRanges = value; - this._onDidChange.fire({ searchInRanges: value }); + set findScope(value: INotebookFindScope) { + if (this._findScope !== value) { + this._findScope = value; + this._onDidChange.fire({ findScope: true }); } } - private _selectedRanges: ICellRange[] = []; - - get selectedRanges(): ICellRange[] { - return this._selectedRanges; - } - - set selectedRanges(value: ICellRange[]) { - if (this._selectedRanges !== value) { - this._selectedRanges = value; - this._onDidChange.fire({ searchInRanges: this._searchInRanges }); - } - } private readonly _initialMarkupInput: boolean; private readonly _initialMarkupPreview: boolean; @@ -106,8 +94,7 @@ export class NotebookFindFilters extends Disposable { markupPreview: boolean, codeInput: boolean, codeOutput: boolean, - searchInRanges: boolean, - selectedRanges: ICellRange[] + findScope: INotebookFindScope ) { super(); @@ -115,8 +102,7 @@ export class NotebookFindFilters extends Disposable { this._markupPreview = markupPreview; this._codeInput = codeInput; this._codeOutput = codeOutput; - this._searchInRanges = searchInRanges; - this._selectedRanges = selectedRanges; + this._findScope = findScope; this._initialMarkupInput = markupInput; this._initialMarkupPreview = markupPreview; @@ -125,7 +111,7 @@ export class NotebookFindFilters extends Disposable { } isModified(): boolean { - // do not include searchInRanges or selectedRanges in the check. This will incorrectly mark the filter icon as modified + // 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 @@ -139,7 +125,6 @@ export class NotebookFindFilters extends Disposable { this._markupPreview = v.markupPreview; this._codeInput = v.codeInput; this._codeOutput = v.codeOutput; - this._searchInRanges = v.searchInRanges; - this._selectedRanges = v.selectedRanges; + 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 6fa5a4fea6ca5..214de0211a6a7 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 de522b344a5eb..0ca2d422f5313 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,7 +115,7 @@ export class FindModel extends Disposable { } private _updateCellStates(e: FindReplaceStateChangedEvent) { - if (!this._state.filters?.markupInput || !this._state.filters?.markupPreview || !this._state.filters?.searchInRanges || !this._state.filters?.selectedRanges) { + if (!this._state.filters?.markupInput || !this._state.filters?.markupPreview || !this._state.filters?.findScope) { return; } @@ -127,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, @@ -136,8 +136,7 @@ export class FindModel extends Disposable { includeCodeInput: false, includeMarkupPreview: false, includeOutput: false, - searchInRanges: this._state.filters?.searchInRanges, - selectedRanges: this._state.filters?.selectedRanges + findScope: this._state.filters?.findScope, }; const contentMatches = viewModel.find(this._state.searchString, options); @@ -476,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, @@ -485,8 +484,7 @@ export class FindModel extends Disposable { includeCodeInput: this._state.filters?.codeInput ?? true, includeMarkupPreview: !!this._state.filters?.markupPreview, includeOutput: !!this._state.filters?.codeOutput, - searchInRanges: this._state.filters?.searchInRanges, - selectedRanges: this._state.filters?.selectedRanges + 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 fe115e230810f..d61cc7974978e 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 fd39a06a3f720..fb3d93d9d5e92 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,13 +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'; -import { INotebookCommandContext, NotebookMultiCellAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; registerNotebookContribution(NotebookFindContrib.id, NotebookFindContrib); @@ -78,12 +78,7 @@ registerAction2(class extends NotebookMultiCellAction { } const controller = editor.getContribution(NotebookFindContrib.id); - - if (context.selectedCells.length > 1) { - controller.show(undefined, { searchInRanges: true, selectedRanges: editor.getSelections() }); - } else { - controller.show(undefined, { searchInRanges: false, selectedRanges: [] }); - } + controller.show(undefined, { findScope: { findScopeType: NotebookFindScopeType.None } }); } }); 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 7b261f5dc0e39..8c4b59a688e43 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -3,55 +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, findSelectionIcon, SimpleButton } from 'vs/editor/contrib/find/browser/findWidget'; -import * as nls from 'vs/nls'; -import { ContextScopedReplaceInput, registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; +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 { INotebookDeltaDecoration, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { IToggleStyles, Toggle } 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 { asCssVariable, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; +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_FIND_IN_CELL_SELECTION_BTN_LABEL = nls.localize('label.findInCellSelectionButton', "Find in Cell Selection"); +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"); @@ -66,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) { @@ -319,8 +320,8 @@ export abstract class SimpleFindReplaceWidget extends Widget { private _filters: NotebookFindFilters; private readonly inSelectionToggle: Toggle; - private searchInSelectionEnabled: boolean; - private selectionDecorationIds: string[] = []; + private cellSelectionDecorationIds: string[] = []; + private textSelectionDecorationIds: ICellModelDecorations[] = []; constructor( @IContextViewService private readonly _contextViewService: IContextViewService, @@ -341,7 +342,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { codeOutput: boolean; }>(NotebookSetting.findFilters) ?? { markupSource: true, markupPreview: true, codeSource: true, codeOutput: true }; - this._filters = new NotebookFindFilters(findFilters.markupSource, findFilters.markupPreview, findFilters.codeSource, findFilters.codeOutput, false, []); + 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(() => { @@ -386,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 => { @@ -462,22 +464,54 @@ export abstract class SimpleFindReplaceWidget extends Widget { this.inSelectionToggle = this._register(new Toggle({ icon: findSelectionIcon, - title: NLS_FIND_IN_CELL_SELECTION_BTN_LABEL, + 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; - this._filters.searchInRanges = checked; if (checked) { - this._filters.selectedRanges = this._notebookEditor.getSelections(); - this.setCellSelectionDecorations(); + // 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.selectedRanges = []; + this._filters.findScope = { + findScopeType: NotebookFindScopeType.None + }; this.clearCellSelectionDecorations(); + this.clearTextSelectionDecorations(); } }); @@ -496,22 +530,6 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._innerFindDomNode.appendChild(this.inSelectionToggle.domNode); this._innerFindDomNode.appendChild(closeBtn.domNode); - this.searchInSelectionEnabled = this._configurationService.getValue(NotebookSetting.findScope); - this.inSelectionToggle.domNode.style.display = this.searchInSelectionEnabled ? 'inline' : 'none'; - - this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(NotebookSetting.findScope)) { - this.searchInSelectionEnabled = this._configurationService.getValue(NotebookSetting.findScope); - if (this.searchInSelectionEnabled) { - this.inSelectionToggle.domNode.style.display = 'inline'; - } else { - this.inSelectionToggle.domNode.style.display = 'none'; - this.inSelectionToggle.checked = false; - this.clearCellSelectionDecorations(); - } - } - }); - // _domNode wraps _innerDomNode, ensuring that this._domNode.appendChild(this._innerFindDomNode); @@ -704,11 +722,37 @@ export abstract class SimpleFindReplaceWidget extends Widget { options: { className: 'nb-multiCellHighlight', outputClassName: 'nb-multiCellHighlight' } } satisfies INotebookDeltaDecoration); } - this.selectionDecorationIds = this._notebookEditor.deltaCellDecorations([], decorations); + this.cellSelectionDecorationIds = this._notebookEditor.deltaCellDecorations([], decorations); } private clearCellSelectionDecorations() { - this._notebookEditor.deltaCellDecorations(this.selectionDecorationIds, []); + 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 { @@ -717,9 +761,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { override dispose() { super.dispose(); - if (this._domNode && this._domNode.parentElement) { - this._domNode.parentElement.removeChild(this._domNode); - } + this._domNode.remove(); } public getDomNode() { @@ -750,20 +792,11 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._findInput.focus(); } - public show(initialInput?: string, options?: { focus?: boolean; searchInRanges?: boolean; selectedRanges?: ICellRange[] }): void { + public show(initialInput?: string, options?: IShowNotebookFindWidgetOptions): void { if (initialInput) { this._findInput.setValue(initialInput); } - if (this.searchInSelectionEnabled && options?.searchInRanges !== undefined) { - this._filters.searchInRanges = options.searchInRanges; - this.inSelectionToggle.checked = options.searchInRanges; - if (options.searchInRanges && options.selectedRanges) { - this._filters.selectedRanges = options.selectedRanges; - this.setCellSelectionDecorations(); - } - } - this._isVisible = true; setTimeout(() => { @@ -812,7 +845,10 @@ export abstract class SimpleFindReplaceWidget extends Widget { public hide(): void { if (this._isVisible) { this.inSelectionToggle.checked = false; - this._notebookEditor.deltaCellDecorations(this.selectionDecorationIds, []); + 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'); 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 c4307db60b983..15954bc2b8164 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -25,8 +25,8 @@ 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'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; @@ -40,8 +40,7 @@ export interface IShowNotebookFindWidgetOptions { matchIndex?: number; focus?: boolean; searchStringSeededFrom?: { cell: ICellViewModel; range: Range }; - searchInRanges?: boolean; - selectedRanges?: ICellRange[]; + findScope?: INotebookFindScope; } export class NotebookFindContrib extends Disposable implements INotebookEditorContribution { @@ -348,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/navigation/arrow.ts b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index e6656eaac7324..203839dfd8583 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 c05cdaae7f6f8..5ca97b08f958c 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 1a0f19d4a5802..c9d3c97a77282 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); - *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); + 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 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 { + readonly outlineKind = 'notebookCells'; - private readonly _dispoables = new DisposableStore(); + 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; - get entries(): OutlineEntry[] { - return this._outlineProviderReference?.object?.entries ?? []; - } - - private readonly _entriesDisposables = new DisposableStore(); + 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; - readonly outlineKind = 'notebookCells'; + // 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 eb2d57e62fe9d..d5899e3198684 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts @@ -499,7 +499,7 @@ export 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 690a05c19fbbe..22b0a5e3fe311 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 1d5d4405f0693..e69da8b479d5e 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 0db8e6dd2b455..310a367f197ff 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 7ed5b3c56832f..f474b4dfc6b43 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_AGENT, 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_AGENT, + 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_AGENT, + 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_AGENT, + 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_AGENT, + 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_AGENT, + 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 f35264aa8cc80..d642cbdcda90a 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 @@ -4,37 +4,34 @@ *--------------------------------------------------------------------------------------------*/ 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 { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import 'vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions'; -import { NotebookChatController } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController'; -import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { CTX_NOTEBOOK_CHAT_HAS_AGENT } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; -class NotebookChatVariables extends Disposable implements IWorkbenchContribution { +class NotebookChatContribution extends Disposable implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.notebookChatVariables'; + static readonly ID = 'workbench.contrib.notebookChatContribution'; + + private readonly _ctxHasProvider: IContextKey; constructor( - @IChatVariablesService private readonly _chatVariableService: IChatVariablesService, - @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService + @IContextKeyService contextKeyService: IContextKeyService, + @IChatAgentService chatAgentService: IChatAgentService ) { super(); - this._register(this._chatVariableService.registerVariable( - { id: '_notebookChatInput', name: '_notebookChatInput', description: '', hidden: true }, - async (_message, _arg, model) => { - const editors = this._notebookEditorService.listNotebookEditors(); - for (const editor of editors) { - const chatController = editor.getContribution(NotebookChatController.id) as NotebookChatController | undefined; - if (chatController?.hasSession(model)) { - return chatController.getSessionInputUri(); - } - } - - return undefined; - } - )); + 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(NotebookChatVariables.ID, NotebookChatVariables, WorkbenchPhase.BlockRestore); +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 4c8a6aa024d0c..259b8e8303edc 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 4662ab11501b1..3de91da698d74 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -26,7 +26,6 @@ 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'; @@ -37,7 +36,6 @@ 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'; @@ -413,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' } } } @@ -469,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) { @@ -546,9 +562,6 @@ export class NotebookChatController extends Disposable implements INotebookEdito 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); @@ -664,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 3cc7faf8354db..8ef65e7a12137 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 c73973e433ee5..82b3f2b8eafc6 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/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 59a4d27a8e245..263d08b0c14f1 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 00c89e900fae1..ea94a04d2adae 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 29dc02777d3a4..dd98ad9d3a4ba 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/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 0f9026c0dde80..4e174318c1127 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 { @@ -1055,11 +1055,6 @@ configurationRegistry.registerConfiguration({ }, tags: ['notebookLayout'] }, - [NotebookSetting.findScope]: { - markdownDescription: nls.localize('notebook.experimental.find.scope.enabled', "Enables the user to search within a selection of cells in the notebook. When enabled, the user will see a \"Find in Cell Selection\" icon in the notebook find widget."), - type: 'boolean', - default: false, - }, [NotebookSetting.remoteSaving]: { markdownDescription: nls.localize('notebook.remoteSaving', "Enables the incremental saving of notebooks between processes and across Remote connections. When enabled, only the changes to the notebook are sent to the extension host, improving performance for large notebooks and slow network connections."), type: 'boolean', diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityProvider.ts index 948db4cd3a1c8..33a38169f3973 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/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index f474d202cb195..b0abb66c5fb3b 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 2f83ab690ff5a..072a2a70593ba 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -74,7 +74,7 @@ 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'; @@ -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(); @@ -1435,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); @@ -1826,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); } @@ -2552,7 +2567,7 @@ 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 []; } @@ -2563,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; } @@ -2587,11 +2602,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return []; } - const selectedRanges = options.selectedRanges?.map(range => this._notebookViewModel?.validateRange(range)).filter(range => !!range); - const selectedIndexes = cellRangesToIndexes(selectedRanges ?? []); - const findIds: string[] = selectedIndexes.map(index => this._notebookViewModel?.viewCells[index].id ?? ''); + 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: options.searchInRanges ? findIds : [] }); + 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 []; @@ -3210,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 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 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 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 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 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 focusedEditorBorderColor = registerColor('notebook.focusedEditorBorder', focusBorder, nls.localize('notebook.focusedEditorBorder', "The color of the notebook cell editor border.")); -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 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', debugIconStartForeground, nls.localize('notebookEditorOverviewRuler.runningCellForeground', "The color of the running cell decoration in the notebook editor overview ruler.")); + +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', foreground, nls.localize('notebookStatusRunningIcon.foreground', "The running icon color of notebook cells in the cell status bar.")); + +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', 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', { @@ -3267,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, @@ -3303,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)), @@ -3324,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 listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', { - dark: scrollbarSliderBackground, - light: scrollbarSliderBackground, - hcDark: scrollbarSliderBackground, - hcLight: 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 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 cellInsertionIndicator = registerColor('notebook.cellInsertionIndicator', focusBorder, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); + +export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', scrollbarSliderBackground, nls.localize('notebookScrollbarSliderBackground', "Notebook scrollbar slider background color.")); + +export const listScrollbarSliderHoverBackground = registerColor('notebookScrollbarSlider.hoverBackground', scrollbarSliderHoverBackground, nls.localize('notebookScrollbarSliderHoverBackground', "Notebook scrollbar slider background color when hovering.")); + +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 20ffe3867e833..63c683890808f 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 46ce122a0044b..c83be8c872fce 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 4be07da63b7b4..5b1360f0ca8f7 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 3e3446349b801..0c3852191dac5 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 572a833e173d2..05174803267cb 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 6a0fd2912769e..86a0b01c5604b 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -132,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 })); } } @@ -171,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(), @@ -197,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 }; }; @@ -639,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: [] }); @@ -697,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 546637fae83d1..1ae8bf98ef5df 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 854358af69d40..bfb757be79c4a 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 b8ccbf07abf01..dcdd1a088604e 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; - } - - 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; - } - - await this._commentThreadWidget.updateCommentThread(info.thread); - this.currentElement.commentHeight = this._calculateCommentThreadHeight(this._commentThreadWidget.getDimensions().height); - } + this.cellDisposables.add(this.commentService.onDidUpdateCommentThreads(async () => this._updateThread())); + } + + 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; + } + + 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 06444079e65ed..407fc5eaa81f5 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/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 6cb5b68da50bc..5300567754b44 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 98b202b5b1c4d..a4514e41d4d8d 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 733ac19d6bb7f..36911bfbdbb0c 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 45ccce3765874..4370f0365584f 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 de2c0e912bf84..c6f345362ea98 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 b1a3e8b3e9374..7fbe705a0a0f0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts @@ -349,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 9cc82bc223a00..51c11f433832d 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({ @@ -436,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; @@ -1119,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 }); } 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 9f6f5b8dac591..564ddd0641403 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/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index fb7dffded14d3..f80e4f40ccc8c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -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); } } }); @@ -2755,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() { @@ -2941,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, }); @@ -3069,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 f946c2142d999..9ed29128b2cff 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 e8cf31df9e38b..5c523a51fac74 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 3f5a6c3e68d49..3ef89aa05e30b 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 0f255cc20db21..3652f955558a6 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,51 +207,59 @@ 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 0000000000000..767b9f8f8ded3 --- /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 0000000000000..c4d2f82593acb --- /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 77a8ba22ab4e1..a7de885758416 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 1ac843597abab..0000000000000 --- 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 54411bcd29679..0000000000000 --- 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 8a0f837901156..60a3626484fac 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,13 +910,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD } //#region Find - find(value: string, options: INotebookSearchOptions): CellFindMatchWithIndex[] { + find(value: string, options: INotebookFindOptions): CellFindMatchWithIndex[] { const matches: CellFindMatchWithIndex[] = []; let findCells: CellViewModel[] = []; - const selectedRanges = options.selectedRanges?.map(range => this.validateRange(range)).filter(range => !!range); - - if (options.searchInRanges && selectedRanges) { + 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 { diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index 52e6889c4bc76..50a6758941bb8 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 6348c0d359f95..ea1039d672cf1 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 25feb7f91233d..8874848aabbe5 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 9856cbf900b4c..19b4df7f19330 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 b3b6ff6e2f9d9..21870a0067fdd 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 33c85b2742097..aea2596da4832 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 { 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'; -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'; 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,8 +838,19 @@ export interface INotebookSearchOptions { includeMarkupPreview?: boolean; includeCodeInput?: boolean; includeOutput?: boolean; - searchInRanges?: boolean; - selectedRanges?: ICellRange[]; + findScope?: INotebookFindScope; +} + +export interface INotebookFindScope { + findScopeType: NotebookFindScopeType; + selectedCellRanges?: ICellRange[]; + selectedTextRanges?: Range[]; +} + +export enum NotebookFindScopeType { + Cells = 'cells', + Text = 'text', + None = 'none' } export interface INotebookExclusiveDocumentFilter { @@ -954,7 +976,6 @@ export const NotebookSetting = { outputFontFamilyDeprecated: 'notebook.outputFontFamily', outputFontFamily: 'notebook.output.fontFamily', findFilters: 'notebook.find.filters', - findScope: 'notebook.experimental.find.scope.enabled', 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 4aad255f78761..e659674d527f7 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 0ac6de7110079..5edcb35f62c2e 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 3c26eff229a02..a6a06d6318600 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 3eff1eb5060bb..dc3d26a0ca390 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 9d0a90e457608..d43273d4fa61e 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 be999203d7524..2351e4b42b228 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 16a9ee3a57b2a..e2911dcf9e1aa 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 7709674b93787..50e49939035e6 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 17fbe22abaac9..7063f68850544 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 0db4fb1920108..5867385c0aa0c 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 357927dbf609d..65ea08b7f2773 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 0000000000000..f08ca269a47ac --- /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 fefb3d36cff61..13b2d31e380de 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 80931b652e631..342bbd90a937b 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 c8f3a95808523..7f9571b91e19c 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 886806afb688f..11a24c15fecc1 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 ef1c965adb22f..86636c92a0ba0 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 de3e3c30e92e1..501dcb337c99e 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 9145f0bf00bd9..ed6e5f6363bde 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 e9d26500bed58..8f4eb76f909be 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 90d732a0cf1c6..dfe5b81e3f44d 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 98628d0c1b945..0770113f20a3f 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 066d89cc736b1..ade4aa5abc3d9 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 9b6eb24d20806..dedeec1068bca 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 782c8145df28d..b2dfc2be7ffdb 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 6f9de776b5334..f62c916928ea4 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 d18126e162b19..d0c5bc4bd91fc 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 654fe7ee807a4..f2bb130dba913 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 1bd5ca8f03c87..8bfdf0497013a 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 071017d82d669..a7849fd73195a 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, + this.serializer, + { + id: new ExtensionIdentifier('test'), + location: undefined + } + ); + } + override tryGetDataProviderSync(viewType: string): SimpleNotebookProviderInfo | undefined { + if (!this.serializer) { + return undefined; + } return new SimpleNotebookProviderInfo( notebook.viewType, - serializer, + 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 c4b0052845042..09731c28e5d0e 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 7087aa9ec1aed..8dd95c484df6d 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 4acfd482bf8bb..0d397b63bf6f7 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 a476f5caeadbc..a037768bbed9f 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 1ed657a1972bf..b05cd81e6a7c8 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 db9868edfa7ae..d40658ab3535c 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 5bbbf50a502a7..0fd1c9a92fe6a 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 647c96305ce73..77ce4841a79b1 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 0cb027d1beb7f..f8dae4867360c 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 4c250a43ab101..abcb1828caa59 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 fa082d78e2353..ee359d70e9d0e 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 f226b1c608ae3..c9d92f4a71b2b 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 f2af8c327470c..199ee022a90c1 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 34c0a8c1a01e8..8d530e9c21cdb 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 6db24ac2b1092..117ce7aaeda0d 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/output/browser/outputLinkProvider.ts b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts index ca06b37e21f2f..3b82ad1889e8e 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 eb83c902631f7..6521eeec89184 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 19e5642f145fc..9a23e945d8512 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 2f7988f5236f7..722c80940c957 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/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 2c8e669e7a0bb..6f3c50861869d 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 })); @@ -1213,7 +1213,7 @@ class WhenColumnRenderer 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 74caeb8c44e9b..b42d6a194a7e6 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 66ca47c40e5bf..ccf4663736cd5 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 1ce23e6167835..dc76d156be6c8 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 585b89b4014b9..4e68e88dc97cd 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 0b0441c93560e..3584e152a210a 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 } }); @@ -1301,6 +1304,7 @@ export class SettingsEditor2 extends EditorPane { const toggleData = await getExperimentalExtensionToggleData(this.extensionGalleryService, this.productService); if (toggleData && groups.filter(g => g.extensionInfo).length) { for (const key in toggleData.settingsEditorRecommendedExtensions) { + const recommendationInfo = toggleData.settingsEditorRecommendedExtensions[key]; const extension = toggleData.recommendedExtensionsGalleryInfo[key]; let manifest: IExtensionManifest | null = null; try { @@ -1327,16 +1331,17 @@ export class SettingsEditor2 extends EditorPane { keyRange: nullRange, value: null, valueRange: nullRange, - description: [extension?.description || ''], + description: [recommendationInfo.onSettingsEditorOpen?.descriptionOverride ?? extension.description], descriptionIsMarkdown: false, descriptionRanges: [], - title: extensionName, scope: ConfigurationScope.WINDOW, type: 'null', displayExtensionId: extension.identifier.id, prereleaseExtensionId: key, stableExtensionId: key, - extensionGroupTitle: groupTitle ?? extensionName + extensionGroupTitle: groupTitle ?? extensionName, + categoryLabel: 'Extensions', + title: extensionName }; const additionalGroup = this.addOrRemoveManageExtensionSetting(setting, extension, groups); if (additionalGroup) { @@ -1405,9 +1410,7 @@ export class SettingsEditor2 extends EditorPane { keys.forEach(key => this.settingsTreeModel.updateElementsByName(key)); } - // Attempt to render the tree once rather than - // once for each key to avoid redundant calls to this.refreshTree() - this.renderTree(); + keys.forEach(key => this.renderTree(key)); } else { this.renderTree(); } @@ -1447,7 +1450,6 @@ export class SettingsEditor2 extends EditorPane { // update `list`s live, as they have a separate "submit edit" step built in before this (focusedSetting.parentElement && !focusedSetting.parentElement.classList.contains('setting-item-list')) ) { - this.updateModifiedLabelForKey(key); this.scheduleRefresh(focusedSetting, key); return; @@ -1463,8 +1465,10 @@ export class SettingsEditor2 extends EditorPane { if (key) { const elements = this.currentSettingsModel.getElementsByName(key); if (elements && elements.length) { - // TODO https://github.com/microsoft/vscode/issues/57360 - this.refreshTree(); + if (elements.length >= 2) { + console.warn('More than one setting with key ' + key + ' found'); + } + this.refreshSingleElement(elements[0]); } else { // Refresh requested for a key that we don't know about return; @@ -1480,6 +1484,12 @@ export class SettingsEditor2 extends EditorPane { return !!DOM.findParentWithClass(this.rootElement.ownerDocument.activeElement, 'context-view'); } + private refreshSingleElement(element: SettingsTreeSettingElement): void { + if (this.isVisible()) { + this.settingsTree.rerender(element); + } + } + private refreshTree(): void { if (this.isVisible()) { this.settingsTree.setChildren(null, createGroupIterator(this.currentSettingsModel.root)); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 6db2e7883d966..59b31491a53ae 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 80ce14bcaba20..362e608fe189b 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,17 +1334,31 @@ abstract class AbstractSettingObjectRenderer extends AbstractSettingRenderer imp } this.addSettingElementFocusHandler(template); + return template; + } + + renderElement(element: ITreeNode, index: number, templateData: ISettingObjectItemTemplate): void { + super.renderSettingElement(element, index, templateData); + } +} + +class SettingObjectRenderer extends AbstractSettingObjectRenderer implements ITreeRenderer { + override templateId = SETTINGS_OBJECT_TEMPLATE_ID; + renderTemplate(container: HTMLElement): ISettingObjectItemTemplate { + const common = this.renderCommonTemplate(null, container, 'list'); + const widget = this._instantiationService.createInstance(ObjectSettingDropdownWidget, common.controlElement); + const template = this.renderTemplateWithWidget(common, widget); 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)!; + 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 ?? {} : {}; @@ -1299,75 +1367,65 @@ abstract class AbstractSettingObjectRenderer extends AbstractSettingRenderer imp ? template.context.scopeValue ?? {} : {}; - const newValue: Record = {}; + 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 (isDefined(e.item) && e.targetIndex === idx) { - newValue[e.item.key.data] = e.item.value.data; - newItems.push(e.item); + 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 (isUndefinedOrNull(e.item) || e.item.key.data !== item.key.data) { + 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 (isUndefinedOrNull(e.item)) { - delete newValue[e.originalItem.key.data]; + 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 === e.originalItem.key.data); - const defaultItemValue = defaultValue[e.originalItem.key.data] as string | boolean; + const itemToDelete = newItems.findIndex(item => item.key.data === objectKey); + const defaultItemValue = defaultValue[objectKey] as string | boolean; - // Item does not have a default - if (isUndefinedOrNull(defaultValue[e.originalItem.key.data]) && itemToDelete > -1) { + // Item does not have a default or default is bing removed + if (removingDefaultValue || isUndefinedOrNull(defaultValue[objectKey]) && itemToDelete > -1) { newItems.splice(itemToDelete, 1); - } else if (itemToDelete > -1) { + } else if (!removingDefaultValue && 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); + 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) { + if (scopeValue[key] !== value && defaultValue[key] === value && !(settingSupportsRemoveDefault && value === null)) { 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.objectDropdownWidget!.setValue(newItems); template.onChange?.(newObject); } } - renderElement(element: ITreeNode, index: number, templateData: ISettingObjectItemTemplate): void { - super.renderSettingElement(element, index, templateData); - } -} - -class SettingObjectRenderer extends AbstractSettingObjectRenderer implements ITreeRenderer { - override templateId = SETTINGS_OBJECT_TEMPLATE_ID; - - renderTemplate(container: HTMLElement): ISettingObjectItemTemplate { - const common = this.renderCommonTemplate(null, container, 'list'); - const widget = this._instantiationService.createInstance(ObjectSettingDropdownWidget, common.controlElement); - return this.renderTemplateWithWidget(common, widget); - } - protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingObjectItemTemplate, onChange: (value: Record | undefined) => void): void { const items = getObjectDisplayValue(dataElement); const { key, objectProperties, objectPatternProperties, objectAdditionalProperties } = dataElement.setting; @@ -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')); @@ -1905,7 +2008,7 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre type ManageExtensionClickTelemetryClassification = { extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension the user went to manage.' }; owner: 'rzhao271'; - comment: 'Event used to gain insights into when users are using an experimental extension management setting'; + comment: 'Event used to gain insights into when users interact with an extension management setting'; }; export class SettingsExtensionToggleRenderer extends AbstractSettingRenderer implements ITreeRenderer { @@ -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 e30c3bb549a12..eccc736095a45 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. @@ -203,7 +203,7 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { private initLabels(): void { if (this.setting.title) { this._displayLabel = this.setting.title; - this._displayCategory = ''; + this._displayCategory = this.setting.categoryLabel ?? null; return; } const displayKeyFormat = settingKeyToDisplayFormat(this.setting.key, this.parent!.id, this.setting.isLanguageTagSetting); @@ -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 c2f0f7b77597c..6d2472b494bc4 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._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) + tooltip: this.getLocalizedStrings().resetActionTooltip, + run: () => this._onDidChangeList.fire({ type: 'reset', originalItem: item, targetIndex: idx }) }); - } else { + } + + if (item.removable) { actions.push({ - class: ThemeIcon.asClassName(settingsDiscardIcon), + class: ThemeIcon.asClassName(settingsRemoveIcon), enabled: true, - id: 'workbench.action.resetListItem', + id: 'workbench.action.removeListItem', label: '', - tooltip: this.getLocalizedStrings().resetActionTooltip, - run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) + tooltip: this.getLocalizedStrings().deleteActionTooltip, + 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); - -export class PreferencesContribution implements IWorkbenchContribution { +export class PreferencesContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.preferences'; private editorOpeningListener: IDisposable | undefined; - private settingsListener: IDisposable; constructor( - @IModelService private readonly modelService: IModelService, - @ITextModelService private readonly textModelResolverService: ITextModelService, + @IFileService fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IPreferencesService private readonly preferencesService: IPreferencesService, - @ILanguageService private readonly languageService: ILanguageService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @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, ) { - this.settingsListener = this.configurationService.onDidChangeConfiguration(e => { + super(); + this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(USE_SPLIT_JSON_SETTING) || e.affectsConfiguration(DEFAULT_SETTINGS_EDITOR_SETTING)) { this.handleSettingsEditorRegistration(); } - }); + })); this.handleSettingsEditorRegistration(); - this.start(); + const fileSystemProvider = this._register(this.instantiationService.createInstance(SettingsFileSystemProvider)); + this._register(fileService.registerProvider(SettingsFileSystemProvider.SCHEMA, fileSystemProvider)); } private handleSettingsEditorRegistration(): void { @@ -102,44 +97,13 @@ export class PreferencesContribution implements IWorkbenchContribution { ); } } - - private start(): void { - - this.textModelResolverService.registerTextModelContentProvider('vscode', { - provideTextContent: async (uri: URI): Promise => { - if (uri.scheme !== 'vscode') { - return null; - } - if (uri.authority === 'schemas') { - return this.getSchemaModel(uri); - } - return this.preferencesService.resolveModel(uri); - } - }); - } - - 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 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)); - } - })); - disposables.add(model.onWillDispose(() => disposables.dispose())); - return model; - } - - dispose(): void { + override dispose(): void { dispose(this.editorOpeningListener); - dispose(this.settingsListener); + super.dispose(); } } + 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 29841a282480a..bd5e4ef29c9c3 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/common/settingsFilesystemProvider.ts b/src/vs/workbench/contrib/preferences/common/settingsFilesystemProvider.ts new file mode 100644 index 0000000000000..f0a80db832c36 --- /dev/null +++ b/src/vs/workbench/contrib/preferences/common/settingsFilesystemProvider.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 { NotSupportedError } from 'vs/base/common/errors'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { FileChangeType, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Registry } from 'vs/platform/registry/common/platform'; +import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; + +const schemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); + + +export class SettingsFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { + + static readonly SCHEMA = Schemas.vscode; + + protected readonly _onDidChangeFile = this._register(new Emitter()); + readonly onDidChangeFile = this._onDidChangeFile.event; + + constructor( + @IPreferencesService private readonly preferencesService: IPreferencesService, + @ILogService private readonly logService: ILogService + ) { + super(); + this._register(schemaRegistry.onDidChangeSchema(schemaUri => { + this._onDidChangeFile.fire([{ resource: URI.parse(schemaUri), type: FileChangeType.UPDATED }]); + })); + this._register(preferencesService.onDidDefaultSettingsContentChanged(uri => { + this._onDidChangeFile.fire([{ resource: uri, type: FileChangeType.UPDATED }]); + })); + } + + readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly + FileSystemProviderCapabilities.FileReadWrite; + + async readFile(uri: URI): Promise { + if (uri.scheme !== SettingsFileSystemProvider.SCHEMA) { + throw new NotSupportedError(); + } + let content: string | undefined; + if (uri.authority === 'schemas') { + content = this.getSchemaContent(uri); + } else if (uri.authority === 'defaultsettings') { + content = this.preferencesService.getDefaultSettingsContent(uri); + } + if (content) { + return VSBuffer.fromString(content).buffer; + } + throw FileSystemProviderErrorCode.FileNotFound; + } + + async stat(uri: URI): Promise { + if (schemaRegistry.hasSchemaContent(uri.toString()) || this.preferencesService.hasDefaultSettingsContent(uri)) { + const currentTime = Date.now(); + return { + type: FileType.File, + permissions: FilePermission.Readonly, + mtime: currentTime, + ctime: currentTime, + size: 0 + }; + } + throw FileSystemProviderErrorCode.FileNotFound; + } + + readonly onDidChangeCapabilities = Event.None; + + watch(resource: URI, opts: IWatchOptions): IDisposable { return Disposable.None; } + + async mkdir(resource: URI): Promise { } + async readdir(resource: URI): Promise<[string, FileType][]> { return []; } + + async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { } + async delete(resource: URI, opts: IFileDeleteOptions): Promise { } + + async writeFile() { + throw new NotSupportedError(); + } + + private getSchemaContent(uri: URI): string { + const startTime = Date.now(); + const content = schemaRegistry.getSchemaContent(uri.toString()) ?? '{}' /* Use empty schema if not yet registered */; + const logLevel = this.logService.getLevel(); + if (logLevel === LogLevel.Debug || logLevel === LogLevel.Trace) { + const endTime = Date.now(); + const uncompressed = JSON.stringify(schemaRegistry.getSchemaContributions().schemas[uri.toString()]); + this.logService.debug(`${uri.toString()}: ${uncompressed.length} -> ${content.length} (${Math.round((uncompressed.length - content.length) / uncompressed.length * 100)}%) Took ${endTime - startTime}ms`); + } + return content; + } +} 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 e5b24e6bd3bfd..55c3c4dba66a5 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 6bb2d93fe7da2..c465c0472e18d 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 26d0c30d9cb8d..d8d614fb7d256 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 17b7b7ac4902c..2cb85b71dab2d 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 70a8ba93492ef..b52d115425ca7 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 13ee310b79c2d..98e47977753dd 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 aa8ce0ff5145b..a1b6e3ae2bec3 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -1818,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 0000000000000..d43c6a6e98bfc --- /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 0000000000000..f0f5cd4821eac --- /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 0000000000000..1db3282ad9e3d --- /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 0000000000000..53a0b25667d6c --- /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 0000000000000..bf82d590a26fc --- /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 7ee1d516f6de5..36a6aeffdb351 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); - constructor( - @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 - ) { - this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + private readonly _focusedRepository = observableFromEventOpts( + { owner: this, equalsFn: () => false }, + this.scmViewService.onDidFocusRepository, + () => this.scmViewService.focusedRepository); - const onDidChangeSCMCountBadge = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.countBadge')); - onDidChangeSCMCountBadge(this.renderActivityCount, this, this.disposables); + private readonly _activeEditor = observableFromEventOpts( + { owner: this, equalsFn: () => false }, + this.editorService.onDidActiveEditorChange, + () => this.editorService.activeEditor); - for (const repository of this.scmService.repositories) { - this.onDidAddRepository(repository); + private readonly _activeEditorRepository = derivedObservableWithCache(this, (reader, lastValue) => { + const activeResource = EditorResourceAccessor.getOriginalUri(this._activeEditor.read(reader)); + if (!activeResource) { + return lastValue; } - this.scmViewService.onDidFocusRepository(this.focusRepository, this, this.disposables); - this.focusRepository(this.scmViewService.focusedRepository); + const repository = this.scmService.getRepository(activeResource); + if (!repository) { + return lastValue; + } - editorService.onDidActiveEditorChange(() => this.tryFocusRepositoryBasedOnActiveEditor(), this, this.disposables); - this.renderActivityCount(); - } + return Object.create(repository); + }); - private tryFocusRepositoryBasedOnActiveEditor(repositories: Iterable = this.scmService.repositories): boolean { - const resource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); + /** + * 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]); - if (!resource) { - return false; + 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'); } + }); - let bestRepository: ISCMRepository | null = null; - let bestMatchLength = Number.POSITIVE_INFINITY; + private readonly _countBadge = derived(this, reader => { + let total = 0; - for (const repository of repositories) { - const root = repository.provider.rootUri; + for (const repository of this._countBadgeRepositories.read(reader)) { + const count = repository.count?.read(reader); + const resourceCount = repository.resourceCount.read(reader); - if (!root) { - continue; - } + total = total + (count ?? resourceCount); + } - const path = this.uriIdentityService.extUri.relativePath(root, resource); + return total; + }); - if (path && !/^\.\./.test(path) && path.length < bestMatchLength) { - bestRepository = repository; - bestMatchLength = path.length; - } - } + private _activeRepositoryNameContextKey: IContextKey; + private _activeRepositoryBranchNameContextKey: IContextKey; - if (!bestRepository) { - return false; - } + 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, + @ITitleService private readonly titleService: ITitleService + ) { + super(); - this.focusRepository(bestRepository); - return true; - } + this._activeRepositoryNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryName.bindTo(this.contextKeyService); + this._activeRepositoryBranchNameContextKey = ActiveRepositoryContextKeys.ActiveRepositoryBranchName.bindTo(this.contextKeyService); - private onDidAddRepository(repository: ISCMRepository): void { - const onDidChange = Event.any(repository.provider.onDidChange, repository.provider.onDidChangeResources); - const changeDisposable = onDidChange(() => this.renderActivityCount()); + this.titleService.registerVariables([ + { name: 'activeRepositoryName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryName.key }, + { name: 'activeRepositoryBranchName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryBranchName.key, } + ]); - const onDidRemove = Event.filter(this.scmService.onDidRemoveRepository, e => e === repository); - const removeDisposable = onDidRemove(() => { - disposable.dispose(); - this.repositoryDisposables.delete(disposable); - this.renderActivityCount(); - }); + this._register(autorunWithStore((reader, store) => { + this._updateActivityCountBadge(this._countBadge.read(reader), store); + })); - const disposable = combinedDisposable(changeDisposable, removeDisposable); - this.repositoryDisposables.add(disposable); + this._register(autorunWithStore((reader, store) => { + const repository = this._activeRepository.read(reader); + const commands = repository?.provider.statusBarCommands.read(reader); - this.tryFocusRepositoryBasedOnActiveEditor(Iterable.single(repository)); - } + this._updateStatusBar(repository, commands ?? [], store); + })); - private onDidRemoveRepository(repository: ISCMRepository): void { - if (this.focusedRepository !== repository) { - return; - } + this._register(autorun(reader => { + const repository = this._activeRepository.read(reader); + const currentHistoryItemGroup = repository?.provider.historyProviderObs.read(reader)?.currentHistoryItemGroupObs.read(reader); - this.focusRepository(Iterable.first(this.scmService.repositories)); + this._updateActiveRepositoryContextKeys(repository, currentHistoryItemGroup); + })); } - private focusRepository(repository: ISCMRepository | undefined): void { - if (this.focusedRepository === repository) { - return; - } - - this.focusDisposable.dispose(); - this.focusedRepository = repository; + private _getRepositoryResourceCount(repository: ISCMRepository): IObservable { + return observableFromEvent(this, repository.provider.onDidChangeResources, () => /** @description repositoryResourceCount */ getRepositoryResourceCount(repository.provider)); + } - if (repository && repository.provider.onDidChangeStatusBarCommands) { - this.focusDisposable = repository.provider.onDidChangeStatusBarCommands(() => this.renderStatusBar(repository)); + private _updateActivityCountBadge(count: number, store: DisposableStore): void { + if (count === 0) { + return; } - this.renderStatusBar(repository); - this.renderActivityCount(); + const badge = new NumberBadge(count, num => localize('scmPendingChangesBadge', '{0} pending changes', num)); + store.add(this.activityService.showViewActivity(VIEW_PANE_ID, { badge })); } - 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(); - } -} - -const ActiveRepositoryContextKeys = { - ActiveRepositoryName: new RawContextKey('scmActiveRepositoryName', ''), - ActiveRepositoryBranchName: new RawContextKey('scmActiveRepositoryBranchName', ''), -}; - -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(); + private _updateActiveRepositoryContextKeys(repository: ISCMRepository | undefined, currentHistoryItemGroup: ISCMHistoryItemGroup | undefined): void { + this._activeRepositoryNameContextKey.set(repository?.provider.name ?? ''); + this._activeRepositoryBranchNameContextKey.set(currentHistoryItemGroup?.name ?? ''); } } -export class SCMActiveResourceContextKeyController implements IWorkbenchContribution { +export class SCMActiveResourceContextKeyController extends Disposable implements IWorkbenchContribution { + private readonly _repositories = observableFromEvent(this, + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), + () => this.scmService.repositories); - 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) { + return false; + } - 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; + 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 undefined; + return false; } - private getEditorHasChanges(activeEditor: EditorInput | null): boolean { + private _getEditorRepositoryId(activeEditor: EditorInput | null): string | undefined { 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; - } - } + if (!activeResource) { + return undefined; } - return false; + const activeResourceRepository = this.scmService.getRepository(activeResource); + return activeResourceRepository?.id; } - dispose(): void { - this.disposables.dispose(); - dispose(this.repositoryDisposables.values()); - this.repositoryDisposables.clear(); - this.onDidRepositoryChange.dispose(); + 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 7537f80f534cd..173cdb04fd046 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 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 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 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 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 editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', editorErrorForeground, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); + +const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', editorGutterModifiedBackground, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); + +const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', editorGutterAddedBackground, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added.")); + +const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', editorGutterDeletedBackground, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted.")); + +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 c8939c63fbe40..93728f7132f44 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 @@ -114,6 +110,10 @@ line-height: 22px; } +.scm-view .monaco-list-row .monaco-icon-label-container { + height: 22px; +} + .scm-view .monaco-list-row .history, .scm-view .monaco-list-row .history-item-group, .scm-view .monaco-list-row .resource-group { @@ -133,6 +133,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 +357,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 +496,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 +510,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 915abcb3e32ba..a571965192758 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -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 c60907964f07a..91129c1d6ca3e 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'; @@ -113,13 +113,10 @@ viewsRegistry.registerViews([{ }], viewContainer); Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored); - -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(SCMActiveRepositoryContextKeyController, LifecyclePhase.Restored); + .registerWorkbenchContribution(SCMActiveRepositoryController, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(SCMStatusController, LifecyclePhase.Restored); + .registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored); registerWorkbenchContribution2( SCMWorkingSetController.ID, @@ -352,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 } } }); @@ -386,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, 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 0000000000000..f6505a2eec43c --- /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 890e39b459f00..99e9f61f835c4 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 e33a86c854a46..594d24ef8f3ca 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.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); - }; - - // TODO@joao TODO@lszomoru - let disposed = false; - templateData.elementDisposables.add(toDisposable(() => disposed = true)); - templateData.elementDisposables.add(repository.provider.onDidChange(() => { - if (disposed) { - return; - } - - onDidChangeProvider(); })); - onDidChangeProvider(); - const repositoryMenus = this.scmViewService.menus.getRepositoryMenus(repository.provider); const menu = this.toolbarMenuId === MenuId.SCMTitle ? repositoryMenus.titleMenu.menu : repositoryMenus.repositoryMenu; templateData.elementDisposables.add(connectPrimaryMenu(menu, (primary, secondary) => { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index ca07b9168af4d..7faf93b5efa5f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -10,7 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; 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'; @@ -41,7 +41,6 @@ import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/comm 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 { IMenuWorkbenchToolBarOptions, MenuWorkbenchToolBar, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { editorSelectionBackground, foreground, inputBackground, inputForeground, listActiveSelectionForeground, registerColor, selectionBackground, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { DropdownWithPrimaryActionViewItem } from 'vs/platform/actions/browser/dropdownWithPrimaryActionViewItem'; 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 { 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.")); - -registerColor('scm.historyItemDeletionsForeground', { - dark: 'gitDecoration.deletedResourceForeground', - light: 'gitDecoration.deletedResourceForeground', - hcDark: 'gitDecoration.deletedResourceForeground', - hcLight: '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.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.")); +const historyItemAdditionsForeground = registerColor('scm.historyItemAdditionsForeground', 'gitDecoration.addedResourceForeground', localize('scm.historyItemAdditionsForeground', "History item additions foreground color.")); + +const historyItemDeletionsForeground = registerColor('scm.historyItemDeletionsForeground', 'gitDecoration.deletedResourceForeground', localize('scm.historyItemDeletionsForeground', "History item deletions foreground color.")); + +registerColor('scm.historyItemStatisticsBorder', transparent(foreground, 0.2), localize('scm.historyItemStatisticsBorder', "History item 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; @@ -1072,7 +1201,9 @@ class HistoryItemChangeRenderer implements ICompressibleTreeRenderer { @@ -1084,6 +1215,7 @@ class SeparatorRenderer implements ICompressibleTreeRenderer('scm.experimental.showHistoryGraph') === true ? Codicon.more : Codicon.gear } satisfies IMenuWorkbenchToolBarOptions; + const toolBar = new WorkbenchToolBar(append(element, $('.actions')), options, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService); + templateDisposables.add(toolBar); - return { label, disposables }; + return { label, toolBar, elementDisposables: new DisposableStore(), templateDisposables }; } renderElement(element: ITreeNode, index: number, templateData: SeparatorTemplate, height: number | undefined): void { + const currentHistoryItemGroup = element.element.repository.provider.historyProvider?.currentHistoryItemGroup; + + // Label templateData.label.setLabel(element.element.label, undefined, { title: element.element.ariaLabel }); + + // Toolbar + const contextKeyService = this.contextKeyService.createOverlay([ + ['scmHistoryItemGroupHasRemote', !!currentHistoryItemGroup?.remote], + ]); + const menu = this.menuService.createMenu(MenuId.SCMChangesSeparator, contextKeyService); + templateData.elementDisposables.add(connectPrimaryMenu(menu, (primary, secondary) => { + templateData.toolBar.setActions(primary, secondary, [MenuId.SCMChangesSeparator]); + })); } renderCompressedElements(node: ITreeNode, void>, index: number, templateData: SeparatorTemplate, height: number | undefined): void { throw new Error('Should never happen since node is incompressible'); } - disposeTemplate(templateData: SeparatorTemplate): void { - templateData.disposables.dispose(); + disposeElement(node: ITreeNode, index: number, templateData: SeparatorTemplate, height: number | undefined): void { + templateData.elementDisposables.clear(); } + disposeTemplate(templateData: SeparatorTemplate): void { + templateData.elementDisposables.dispose(); + templateData.templateDisposables.dispose(); + } } class ListDelegate implements IListVirtualDelegate { @@ -1150,6 +1299,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 +1381,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 +1469,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 +1524,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 +1578,9 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider(); readonly onDidChange: Event = this._onDidChange.event; - private readonly repositoryDisposables = new DisposableStore(); + private readonly _disposables = this._register(new MutableDisposable()); constructor( container: HTMLElement, @@ -2057,7 +2236,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], @@ -2065,7 +2244,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); @@ -2095,18 +2274,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(); } @@ -2114,7 +2293,6 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { updateToolbar(); } - } class SCMInputWidgetEditorOptions { @@ -2238,7 +2416,6 @@ class SCMInputWidget { private element: HTMLElement; private editorContainer: HTMLElement; - private placeholderTextContainer: HTMLElement; private readonly inputEditor: CodeEditorWidget; private readonly inputEditorOptions: SCMInputWidgetEditorOptions; private toolbarContainer: HTMLElement; @@ -2329,14 +2506,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 = () => { @@ -2344,8 +2518,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)); @@ -2426,7 +2599,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); @@ -2436,33 +2608,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, this.disposables); + const editorConstructionOptions = this.inputEditorOptions.getEditorConstructionOptions(); this.inputEditor = instantiationService2.createInstance(CodeEditorWidget, this.editorContainer, editorConstructionOptions, codeEditorWidgetOptions); this.disposables.add(this.inputEditor); @@ -2552,7 +2723,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; @@ -2580,10 +2750,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 { @@ -2673,11 +2840,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(); @@ -2779,6 +2941,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, @@ -2895,7 +3058,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); @@ -2956,6 +3120,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); @@ -2973,6 +3140,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) ], @@ -2986,9 +3154,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)) { @@ -3098,6 +3264,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); @@ -3218,16 +3403,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: () => { } }); } @@ -3412,8 +3595,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 { @@ -3601,6 +3784,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) { @@ -3701,13 +3897,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 []; } @@ -3719,16 +3915,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) + }); + } + + // 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; @@ -3919,13 +4164,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') }; } @@ -3963,6 +4210,7 @@ class SCMTreeDataSource implements IAsyncDataSource(), + historyItems2: new Map(), historyItemChanges: new Map() }; } @@ -4052,3 +4300,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 00c333886a6e3..ed24dc67bf039 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 3a023470ea207..274bd713910da 100644 --- a/src/vs/workbench/contrib/scm/browser/workingSet.ts +++ b/src/vs/workbench/contrib/scm/browser/workingSet.ts @@ -3,9 +3,10 @@ * 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'; @@ -24,13 +25,13 @@ 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, @@ -39,69 +40,61 @@ export class SCMWorkingSetController implements IWorkbenchContribution { @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())); - } - - private _onDidChangeConfiguration(): void { - if (!this.configurationService.getValue('scm.workingSets.enabled')) { - this.storageService.remove('scm.workingSets', StorageScope.WORKSPACE); + super(); - this._scmServiceDisposables.clear(); - this._repositoryDisposables.clearAndDisposeAll(); - - return; - } + this._store.add(autorunWithStore((reader, store) => { + if (!this._enabledConfig.read(reader)) { + this.storageService.remove('scm.workingSets', StorageScope.WORKSPACE); + this._repositoryDisposables.clearAndDisposeAll(); + return; + } - this._workingSets = this._loadWorkingSets(); + this._workingSets = this._loadWorkingSets(); - this.scmService.onDidAddRepository(this._onDidAddRepository, this, this._scmServiceDisposables); - this.scmService.onDidRemoveRepository(this._onDidRemoveRepository, this, this._scmServiceDisposables); + this.scmService.onDidAddRepository(this._onDidAddRepository, this, store); + this.scmService.onDidRemoveRepository(this._onDidRemoveRepository, this, store); - 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 currentHistoryItemGroupId = repository.provider.historyProvider.currentHistoryItemGroup.id; - const repositoryWorkingSets = this._workingSets.get(providerKey); + const providerKey = getProviderKey(repository.provider); + 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; + } - if (repositoryWorkingSets.currentHistoryItemGroupId === currentHistoryItemGroupId) { - return; - } + // Editors for the current working set are automatically restored + 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); } @@ -159,9 +152,8 @@ export class SCMWorkingSetController implements IWorkbenchContribution { } } - 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 2cb81effd9130..83d983824f3ed 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 c0a56b7ee90bc..5bcd1c6fbe72b 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -73,14 +73,14 @@ export interface ISCMProvider extends IDisposable { readonly rootUri?: URI; readonly inputBoxTextModel: ITextModel; - readonly count?: number; + readonly count: IObservable; readonly commitTemplate: IObservable; readonly historyProvider?: ISCMHistoryProvider; + 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 5ea28588e217f..b341a2029aec3 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -15,6 +15,8 @@ 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 extends Disposable implements ISCMInput { @@ -132,7 +134,7 @@ class SCMInput extends Disposable implements ISCMInput { } if (!transient) { - this.historyNavigator.add(this._value); + this.historyNavigator.replaceLast(this._value); this.historyNavigator.add(value); this.didChangeHistory = true; } @@ -364,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); @@ -391,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 0000000000000..5c64ccda40249 --- /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 c9a116bdbfe8e..74f304ba7a33a 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 b39946294c80e..4ef1cdae76eb6 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 e80602a3f826e..1da6d744da9fd 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'; @@ -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/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 82c166d796db6..764f8966664a8 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 d7c088c442e05..121512e94bed3 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 69e8a91054004..662d7fb901971 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 d5b801343416e..f8beab04b545e 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; @@ -205,8 +206,7 @@ export class SearchWidget extends Widget { notebookOptions.isInNotebookMarkdownPreview, notebookOptions.isInNotebookCellInput, notebookOptions.isInNotebookCellOutput, - false, - [] + { findScopeType: NotebookFindScopeType.None } )); this._register( @@ -352,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'; @@ -665,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/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 9b3b9e4f4ddd6..2e98bb36cb6d2 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 17f9e88b338f4..5c79a6d804661 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 a90875c1384ff..f30dcaf0de32d 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 e71fab4b13164..600cc67f97aa2 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 4feb3d343ed7c..c35d9b87da683 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 a986de9b0bfb8..ea1241bed901c 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 4651bbc640333..0758b486cf691 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 c0f0f27dbfa18..c3577b4e873d4 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 2085c52e04106..68b40814a2084 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 77dad6efa4a8c..198ec6d581b66 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 82e14ccc1a611..6b5bef90c2a04 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 8c185e766a5c9..d4824fdf3b962 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 5ac2018ef7dad..39b490f2fe3f4 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 ce61496ec51e7..297836af6c83b 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/test/common/speechService.test.ts b/src/vs/workbench/contrib/speech/test/common/speechService.test.ts index d757eace7e08a..16a4f0d9b773a 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 6982a2cd50b1e..8b2dd0dcdd1a7 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 b560ed8999b52..1fd7dd05886b9 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 13993422b73e4..d166a151900fe 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 0e9b7879ba0c1..e11e8a81fc38f 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 08e8292b60c3b..88059901e6430 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 6b41d88c5f781..eba74599af7c9 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 816894f6b321c..7aad1e553070d 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 c397b896093ba..65b2301495f76 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -31,7 +31,7 @@ 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'; @@ -431,13 +431,22 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc ? 'default' : 'custom'; this.telemetryService.publicLog2('window.systemColorTheme', { settingValue, source }); + }>('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 e355b287f63f4..606202e883775 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 aad118a4608d4..06036fcbaae34 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,8 +170,14 @@ __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 @@ -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 c2971c0de2ac7..6c92130ec958c 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) @@ -207,7 +215,19 @@ function Send-Completions { $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 c3ee3fb42d7ef..b531e31130f02 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 96657abf2ac17..3d07aa2d8bc8e 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 466958b03197b..8e9ddcb5b6cb5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -7,7 +7,7 @@ import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal' import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, Disposable, DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { SplitView, Orientation, IView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; -import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { isHorizontal, IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITerminalInstance, Direction, ITerminalGroup, ITerminalInstanceService, ITerminalConfigurationService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; @@ -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(); @@ -285,10 +285,10 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { if (this._container) { this.attachToElement(this._container); } - this._onPanelOrientationChanged.fire(this._terminalLocation === ViewContainerLocation.Panel && this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL); + this._onPanelOrientationChanged.fire(this._terminalLocation === ViewContainerLocation.Panel && isHorizontal(this._panelPosition) ? Orientation.HORIZONTAL : Orientation.VERTICAL); this._register(toDisposable(() => { if (this._container && this._groupElement) { - this._container.removeChild(this._groupElement); + this._groupElement.remove(); this._groupElement = undefined; } })); @@ -466,7 +466,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { if (!this._splitPaneContainer) { this._panelPosition = this._layoutService.getPanelPosition(); this._terminalLocation = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID)!; - const orientation = this._terminalLocation === ViewContainerLocation.Panel && this._panelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; + const orientation = this._terminalLocation === ViewContainerLocation.Panel && isHorizontal(this._panelPosition) ? Orientation.HORIZONTAL : Orientation.VERTICAL; this._splitPaneContainer = this._instantiationService.createInstance(SplitPaneContainer, this._groupElement, orientation); this.terminalInstances.forEach(instance => this._splitPaneContainer!.split(instance, this._activeInstanceIndex + 1)); } @@ -527,7 +527,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { const newTerminalLocation = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID)!; const terminalPositionChanged = newPanelPosition !== this._panelPosition || newTerminalLocation !== this._terminalLocation; if (terminalPositionChanged) { - const newOrientation = newTerminalLocation === ViewContainerLocation.Panel && newPanelPosition === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; + const newOrientation = newTerminalLocation === ViewContainerLocation.Panel && isHorizontal(newPanelPosition) ? Orientation.HORIZONTAL : Orientation.VERTICAL; this._splitPaneContainer.setOrientation(newOrientation); this._panelPosition = newPanelPosition; this._terminalLocation = newTerminalLocation; @@ -563,7 +563,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } private _getOrientation(): Orientation { - return this._getPosition() === Position.BOTTOM ? Orientation.HORIZONTAL : Orientation.VERTICAL; + return isHorizontal(this._getPosition()) ? Orientation.HORIZONTAL : Orientation.VERTICAL; } resizePane(direction: Direction): void { @@ -588,10 +588,12 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { if (shouldResizePart) { + const position = this._getPosition(); const shouldShrink = - (this._getPosition() === Position.LEFT && direction === Direction.Left) || - (this._getPosition() === Position.RIGHT && direction === Direction.Right) || - (this._getPosition() === Position.BOTTOM && direction === Direction.Down); + (position === Position.LEFT && direction === Direction.Left) || + (position === Position.RIGHT && direction === Direction.Right) || + (position === Position.BOTTOM && direction === Direction.Down) || + (position === Position.TOP && direction === Direction.Up); if (shouldShrink) { resizeAmount *= -1; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index f7bc50614cfce..6310485b23ba8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -79,7 +79,7 @@ import { getWorkspaceForTerminal, preparePathForShell } from 'vs/workbench/contr import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { isHorizontal, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { importAMDNodeModule } from 'vs/amdX'; @@ -703,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; } @@ -858,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 @@ -1844,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; - - // 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; - } + // 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; - 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._layoutSettingsChanged = false; + } - this.xterm.raw.resize(cols, rows); - TerminalInstance._lastKnownGridDimensions = { cols, rows }; + 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; @@ -1977,7 +1991,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } this._dimensionsOverride = dimensions; if (immediate) { - this._resizeNow(true); + this._resize(true); } else { this._resize(); } @@ -2296,9 +2310,7 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID } private _clearDropOverlay() { - if (this._dropOverlay && this._dropOverlay.parentElement) { - this._dropOverlay.parentElement.removeChild(this._dropOverlay); - } + this._dropOverlay?.remove(); this._dropOverlay = undefined; } @@ -2401,7 +2413,7 @@ class TerminalInstanceDragAndDropController extends Disposable implements dom.ID private _getViewOrientation(): Orientation { const panelPosition = this._layoutService.getPanelPosition(); const terminalLocation = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID); - return terminalLocation === ViewContainerLocation.Panel && panelPosition === Position.BOTTOM + return terminalLocation === ViewContainerLocation.Panel && isHorizontal(panelPosition) ? Orientation.HORIZONTAL : Orientation.VERTICAL; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 01db326f019be..22204b7a88ebb 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 24dde378f303d..a4b662a065c97 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 eb3dc1c08f32f..ce5bf14c23ca2 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 a17b660a13cbf..b795f91f201ff 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 af9508e1b1f6e..b00f9876d1315 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -208,7 +208,7 @@ export class TerminalService extends Disposable implements ITerminalService { } if (instance?.shellType) { this._terminalShellTypeContextKey.set(instance.shellType.toString()); - } else if (!instance) { + } else if (!instance || !(instance.shellType)) { this._terminalShellTypeContextKey.reset(); } })); @@ -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 a93fccec1b53d..285c7a72ec148 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/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 966c4ead23498..3cf3c286a93d3 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 032610dbea7a5..63283a6a76bbc 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 67136657ac519..8d14cc3a2452b 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 fc4a6a0a0e0ae..81ccf23927fdd 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -209,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 ced2afffa11ef..86a5a037b4dd2 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/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 72335480aee0a..cf2c5cf5486f5 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -81,8 +81,8 @@ export namespace TerminalContextKeys { /** Whether the mouse is within the terminal tabs list. */ export const tabsMouse = new RawContextKey(TerminalContextKeyStrings.TabsMouse, false, true); - /** The shell type of the active terminal, this is set to the last known value when no terminals exist. */ - export const shellType = new RawContextKey(TerminalContextKeyStrings.ShellType, undefined, { type: 'string', description: localize('terminalShellTypeContextKey', "The shell type of the active terminal, this is set to the last known value when no terminals exist.") }); + /** The shell type of the active terminal, this is set if the type can be detected. */ + export const shellType = new RawContextKey(TerminalContextKeyStrings.ShellType, undefined, { type: 'string', description: localize('terminalShellTypeContextKey', "The shell type of the active terminal, this is set if the type can be detected.") }); /** Whether the terminal's alt buffer is active. */ export const altBufferActive = new RawContextKey(TerminalContextKeyStrings.AltBufferActive, false, localize('terminalAltBufferActive', "Whether the terminal's alt buffer is active.")); 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 5ea55926c390e..a15a395d9d859 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 dd74c5860606d..5aa97b16452ee 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 ef6c5b46e76ea..a547d3f68ae0e 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 6055da8a2a958..c2f0de22f7db3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css @@ -4,11 +4,10 @@ *--------------------------------------------------------------------------------------------*/ .monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint { - color: var(--vscode-input-placeholderForeground); + color: var(--vscode-terminal-initialHintForeground); } .monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint a { cursor: pointer; - color: var(--vscode-textLink-foreground); } .monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint a, 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 a2c974a0baa95..ba1325e8af46a 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 82bd0de7ec4c2..21d5f88714554 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.promptInputModel.value || 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 { @@ -199,7 +220,9 @@ class TerminalInitialHintWidget extends Disposable { @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(() => { @@ -229,6 +252,7 @@ class TerminalInitialHintWidget extends Disposable { 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' @@ -237,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(); } })); @@ -312,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 bf95499ee325a..13d70f863522e 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/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 6b04dac4c5435..d53b7cd4968db 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_AGENT } 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_AGENT + 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?.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?.acceptFeedback(); + 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 5734afb1b876c..005fdf6ca7e1c 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) { - - } 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); + 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 { + 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 0000000000000..fdcdf5c006f39 --- /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 8b9aedb6bc917..d28f3cf845505 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/common/terminalInitialHintConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts index 711ebbb18fed7..41567ee5921ea 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/common/terminalInitialHintConfiguration.ts @@ -6,7 +6,6 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { localize } from 'vs/nls'; import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; -import product from 'vs/platform/product/common/product'; export const enum TerminalInitialHintSettingId { Enabled = 'terminal.integrated.initialHint' @@ -17,6 +16,6 @@ export const terminalInitialHintConfiguration: IStringDictionary { 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 83836adc556bb..2321f8edb9695 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/links/browser/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts index d4247bbd835f2..f021a32d23e0b 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 b419b7b94cac5..80c28af629789 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 bc8e99fb00fc5..9f88f2757dbfa 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 5ab1af0d0eb3c..ed805826706e6 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 194ee23694e42..639ec2c883330 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 d46c7f3ae04e8..cd52628b6dc99 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 f465e37f07653..357a6bbee6099 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 90c2efa5e75a0..03e1ccd475b48 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 86972f65fa702..14f61b9868a4f 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 1d74aeaed36e6..61806a01f2505 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -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,8 +58,6 @@ const TOGGLE_INLINE_COMMAND_ID = 'testing.toggleInlineCoverage'; const BRANCH_MISS_INDICATOR_CHARS = 4; export class CodeCoverageDecorations extends Disposable implements IEditorContribution { - private static readonly fileCoverageDecorations = new WeakMap(); - private loadingCancellation?: CancellationTokenSource; private readonly displayedStore = this._register(new DisposableStore()); private readonly hoveredStore = this._register(new DisposableStore()); @@ -84,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); @@ -98,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, coverage.showInline.read(reader)); + this.apply(editor.getModel()!, c.file, c.testId, coverage.showInline.read(reader)); } else { this.clear(); } @@ -125,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(); } })); @@ -213,9 +205,14 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri const todo = [{ line: lineNumber, dir: 0 }]; const toEnable = new Set(); + const ranges = this.editor.getVisibleRanges(); if (!this.coverage.showInline.get()) { - for (let i = 0; i < todo.length && i < MAX_HOVERED_LINES; i++) { + 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)) { @@ -261,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(); } @@ -347,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); } @@ -534,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()); @@ -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,8 +620,8 @@ class CoverageToolbarWidget extends Disposable implements IOverlayWidget { private setActions() { this.actionBar.clear(); - const coverage = this.current; - if (!coverage) { + const current = this.current; + if (!current) { return; } @@ -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(); }); @@ -798,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; @@ -812,26 +809,21 @@ registerAction2(class FilterCoverageToTestInEditor extends Action2 { coverage = uri && testCoverageService.selected.get()?.getUri(uri); } - if (!coverage || !(coverage.isForTest || coverage.perTestData?.size)) { - return; - } - - const options = coverage?.perTestData ?? coverage?.isForTest?.parent.perTestData; - if (!options) { + if (!coverage || !coverage.perTestData?.size) { 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 @@ -844,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) { @@ -859,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 => { @@ -868,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 33060d117ea11..9e752e5f4ef48 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/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 5fa30f14ebd8f..4c3a45db03316 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -313,6 +313,48 @@ } } +.test-output-call-stack { + height: 100%; + + .monaco-list-row { + padding: 0 3px 0 8px; + display: flex; + gap: 3px; + + .location, .label { + white-space: nowrap; + } + + .label { + flex-grow: 1; + } + + .location { + /*Use direction so the source shows elipses on the left*/ + direction: rtl; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.9em; + } + + &.no-source .label { + opacity: 0.7; + } + + &:hover { + .label { + overflow: hidden; + text-overflow: ellipsis; + } + + .location { + overflow: visible; + text-overflow: unset; + } + } + } +} + /** -- filter */ .monaco-action-bar.testing-filter-action-bar { flex-shrink: 0; diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts index 3e4cf9e7131a9..232184a9e5bc0 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 8e67eebd282c9..26ecefd409a7f 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 6851abd333bdd..37cb8843ff5a4 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/testResultsView/testMessageStack.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts new file mode 100644 index 0000000000000..5e9bdfca5e6d2 --- /dev/null +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Range } from 'vs/editor/common/core/range'; +import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { ITestMessageStackFrame } from 'vs/workbench/contrib/testing/common/testTypes'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; + +const stackItemDelegate: IListVirtualDelegate = { + getHeight: () => 22, + getTemplateId: () => 's', +}; + +export class TestResultStackWidget extends Disposable { + private readonly list: WorkbenchList; + private readonly changeStackFrameEmitter = this._register(new Emitter()); + + public readonly onDidChangeStackFrame = this.changeStackFrameEmitter.event; + + constructor( + private readonly container: HTMLElement, + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService labelService: ILabelService, + @IContextMenuService contextMenuService: IContextMenuService + ) { + super(); + + this.list = this._register(instantiationService.createInstance( + WorkbenchList, + 'TestResultStackWidget', + container, + stackItemDelegate, + [instantiationService.createInstance(StackRenderer)], + { + multipleSelectionSupport: false, + accessibilityProvider: { + getWidgetAriaLabel: () => localize('testStackTrace', 'Test stack trace'), + getAriaLabel: (e: ITestMessageStackFrame) => e.position && e.uri ? localize({ + comment: ['{0} is an extension-defined label, then line number and filename'], + key: 'stackTraceLabel', + }, '{0}, line {1} in {2}', e.label, e.position.lineNumber, labelService.getUriLabel(e.uri, { relative: true })) : e.label, + } + } + ) as WorkbenchList); + + this._register(this.list.onDidChangeSelection(e => { + if (e.elements.length) { + this.changeStackFrameEmitter.fire(e.elements[0]); + } + })); + + this._register(dom.addDisposableListener(container, dom.EventType.CONTEXT_MENU, e => { + contextMenuService.showContextMenu({ + getAnchor: () => ({ x: e.x, y: e.y }), + menuId: MenuId.TestCallStackContext + }); + })); + } + + public update(stack: ITestMessageStackFrame[], selection?: ITestMessageStackFrame) { + this.list.splice(0, this.list.length, stack); + this.list.layout(); + + const i = selection && stack.indexOf(selection); + if (i && i !== -1) { + this.list.setSelection([i]); + this.list.setFocus([i]); + // selection is triggered actioning on the call stack from a different + // editor, ensure the stack item is still focused in this editor + this.list.domFocus(); + } + } + + public layout(height?: number, width?: number) { + this.list.layout(height ?? this.container.clientHeight, width); + } +} + +interface ITemplateData { + container: HTMLElement; + label: HTMLElement; + location: HTMLElement; + current?: ITestMessageStackFrame; + disposable: IDisposable; +} + +class StackRenderer implements IListRenderer { + public readonly templateId = 's'; + + constructor( + @ILabelService private readonly labelService: ILabelService, + @IEditorService private readonly openerService: IEditorService, + ) { } + + renderTemplate(container: HTMLElement): ITemplateData { + const label = dom.$('.label'); + const location = dom.$('.location'); + container.appendChild(label); + container.appendChild(location); + const data: ITemplateData = { + container, + label, + location, + disposable: dom.addDisposableListener(container, dom.EventType.CLICK, e => { + if (e.ctrlKey || e.metaKey) { + if (data.current?.uri) { + this.openerService.openEditor({ + resource: data.current.uri, + options: { + selection: data.current.position ? Range.fromPositions(data.current.position) : undefined, + } + }, SIDE_GROUP); + e.preventDefault(); + e.stopPropagation(); + } + } + }), + }; + + return data; + } + + renderElement(element: ITestMessageStackFrame, index: number, templateData: ITemplateData, height: number | undefined): void { + templateData.label.innerText = element.label; + templateData.current = element; + templateData.container.classList.toggle('no-source', !element.uri); + + if (element.uri) { + templateData.location.innerText = this.labelService.getUriBasenameLabel(element.uri); + templateData.location.title = this.labelService.getUriLabel(element.uri, { relative: true }); + if (element.position) { + templateData.location.innerText += `:${element.position.lineNumber}:${element.position.column}`; + } + } + } + + disposeTemplate(templateData: ITemplateData): void { + templateData.disposable.dispose(); + } +} + diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts new file mode 100644 index 0000000000000..32e1721a8bfc4 --- /dev/null +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts @@ -0,0 +1,546 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { Delayer } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { Iterable } from 'vs/base/common/iterator'; +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable, IDisposable, IReference, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { peekViewResultsBackground } from 'vs/editor/contrib/peekView/browser/peekView'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { DetachedProcessInfo } from 'vs/workbench/contrib/terminal/browser/detachedTerminal'; +import { IDetachedTerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { getXtermScaledDimensions } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; +import { TERMINAL_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { colorizeTestMessageInEditor } from 'vs/workbench/contrib/testing/browser/testMessageColorizer'; +import { InspectSubject, MessageSubject, TaskSubject, TestOutputSubject } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject'; +import { Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; +import { ITaskRawOutput, ITestResult, ITestRunTaskResults, LiveTestResult, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; +import { ITestMessage, TestMessageType, getMarkId } from 'vs/workbench/contrib/testing/common/testTypes'; + + +class SimpleDiffEditorModel extends EditorModel { + public readonly original = this._original.object.textEditorModel; + public readonly modified = this._modified.object.textEditorModel; + + constructor( + private readonly _original: IReference, + private readonly _modified: IReference, + ) { + super(); + } + + public override dispose() { + super.dispose(); + this._original.dispose(); + this._modified.dispose(); + } +} + + +export interface IPeekOutputRenderer extends IDisposable { + /** Updates the displayed test. Should clear if it cannot display the test. */ + update(subject: InspectSubject): void; + /** Recalculate content layout. */ + layout(dimension: dom.IDimension): void; + /** Dispose the content provider. */ + dispose(): void; +} + +const commonEditorOptions: IEditorOptions = { + scrollBeyondLastLine: false, + links: true, + lineNumbers: 'off', + glyphMargin: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false + }, + fixedOverflowWidgets: true, + readOnly: true, + minimap: { + enabled: false + }, + wordWrap: 'on', +}; + +const diffEditorOptions: IDiffEditorConstructionOptions = { + ...commonEditorOptions, + enableSplitViewResizing: true, + isInEmbeddedEditor: true, + renderOverviewRuler: false, + ignoreTrimWhitespace: false, + renderSideBySide: true, + useInlineViewWhenSpaceIsLimited: false, + originalAriaLabel: localize('testingOutputExpected', 'Expected result'), + modifiedAriaLabel: localize('testingOutputActual', 'Actual result'), + diffAlgorithm: 'advanced', +}; + + +export class DiffContentProvider extends Disposable implements IPeekOutputRenderer { + private readonly widget = this._register(new MutableDisposable()); + private readonly model = this._register(new MutableDisposable()); + private dimension?: dom.IDimension; + + constructor( + private readonly editor: ICodeEditor | undefined, + private readonly container: HTMLElement, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITextModelService private readonly modelService: ITextModelService, + ) { + super(); + } + + public async update(subject: InspectSubject) { + if (!(subject instanceof MessageSubject)) { + return this.clear(); + } + const message = subject.message; + if (!ITestMessage.isDiffable(message)) { + return this.clear(); + } + + const [original, modified] = await Promise.all([ + this.modelService.createModelReference(subject.expectedUri), + this.modelService.createModelReference(subject.actualUri), + ]); + + const model = this.model.value = new SimpleDiffEditorModel(original, modified); + if (!this.widget.value) { + this.widget.value = this.editor ? this.instantiationService.createInstance( + EmbeddedDiffEditorWidget, + this.container, + diffEditorOptions, + {}, + this.editor, + ) : this.instantiationService.createInstance( + DiffEditorWidget, + this.container, + diffEditorOptions, + {}, + ); + + if (this.dimension) { + this.widget.value.layout(this.dimension); + } + } + + this.widget.value.setModel(model); + this.widget.value.updateOptions(this.getOptions( + isMultiline(message.expected) || isMultiline(message.actual) + )); + } + + private clear() { + this.model.clear(); + this.widget.clear(); + } + + public layout(dimensions: dom.IDimension) { + this.dimension = dimensions; + this.widget.value?.layout(dimensions); + } + + protected getOptions(isMultiline: boolean): IDiffEditorOptions { + return isMultiline + ? { ...diffEditorOptions, lineNumbers: 'on' } + : { ...diffEditorOptions, lineNumbers: 'off' }; + } +} + +class ScrollableMarkdownMessage extends Disposable { + private readonly scrollable: DomScrollableElement; + private readonly element: HTMLElement; + + constructor(container: HTMLElement, markdown: MarkdownRenderer, message: IMarkdownString) { + super(); + + const rendered = this._register(markdown.render(message, {})); + rendered.element.style.height = '100%'; + rendered.element.style.userSelect = 'text'; + container.appendChild(rendered.element); + this.element = rendered.element; + + this.scrollable = this._register(new DomScrollableElement(rendered.element, { + className: 'preview-text', + })); + container.appendChild(this.scrollable.getDomNode()); + + this._register(toDisposable(() => { + this.scrollable.getDomNode().remove(); + })); + + this.scrollable.scanDomNode(); + } + + public layout(height: number, width: number) { + // Remove padding of `.monaco-editor .zone-widget.test-output-peek .preview-text` + this.scrollable.setScrollDimensions({ + width: width - 32, + height: height - 16, + scrollWidth: this.element.scrollWidth, + scrollHeight: this.element.scrollHeight + }); + } +} + +export class MarkdownTestMessagePeek extends Disposable implements IPeekOutputRenderer { + private readonly markdown = new Lazy( + () => this._register(this.instantiationService.createInstance(MarkdownRenderer, {})), + ); + + private readonly textPreview = this._register(new MutableDisposable()); + + constructor(private readonly container: HTMLElement, @IInstantiationService private readonly instantiationService: IInstantiationService) { + super(); + } + + public update(subject: InspectSubject): void { + if (!(subject instanceof MessageSubject)) { + return this.textPreview.clear(); + } + + const message = subject.message; + if (ITestMessage.isDiffable(message) || typeof message.message === 'string') { + return this.textPreview.clear(); + } + + this.textPreview.value = new ScrollableMarkdownMessage( + this.container, + this.markdown.value, + message.message as IMarkdownString, + ); + } + + public layout(dimension: dom.IDimension): void { + this.textPreview.value?.layout(dimension.height, dimension.width); + } +} + +export class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { + private readonly widgetDecorations = this._register(new MutableDisposable()); + private readonly widget = this._register(new MutableDisposable()); + private readonly model = this._register(new MutableDisposable()); + private dimension?: dom.IDimension; + + constructor( + private readonly editor: ICodeEditor | undefined, + private readonly container: HTMLElement, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITextModelService private readonly modelService: ITextModelService, + ) { + super(); + } + + public async update(subject: InspectSubject) { + if (!(subject instanceof MessageSubject)) { + return this.clear(); + } + + const message = subject.message; + if (ITestMessage.isDiffable(message) || message.type === TestMessageType.Output || typeof message.message !== 'string') { + return this.clear(); + } + + const modelRef = this.model.value = await this.modelService.createModelReference(subject.messageUri); + if (!this.widget.value) { + this.widget.value = this.editor ? this.instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this.container, + commonEditorOptions, + {}, + this.editor, + ) : this.instantiationService.createInstance( + CodeEditorWidget, + this.container, + commonEditorOptions, + { isSimpleWidget: true } + ); + + if (this.dimension) { + this.widget.value.layout(this.dimension); + } + } + + this.widget.value.setModel(modelRef.object.textEditorModel); + this.widget.value.updateOptions(commonEditorOptions); + this.widgetDecorations.value = colorizeTestMessageInEditor(message.message, this.widget.value); + } + + private clear() { + this.widgetDecorations.clear(); + this.widget.clear(); + this.model.clear(); + } + + public layout(dimensions: dom.IDimension) { + this.dimension = dimensions; + this.widget.value?.layout(dimensions); + } +} + +export class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { + private dimensions?: dom.IDimension; + private readonly terminalCwd = this._register(new MutableObservableValue('')); + private readonly xtermLayoutDelayer = this._register(new Delayer(50)); + + /** Active terminal instance. */ + private readonly terminal = this._register(new MutableDisposable()); + /** Listener for streaming result data */ + private readonly outputDataListener = this._register(new MutableDisposable()); + + constructor( + private readonly container: HTMLElement, + private readonly isInPeekView: boolean, + @ITerminalService private readonly terminalService: ITerminalService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService, + ) { + super(); + } + + private async makeTerminal() { + const prev = this.terminal.value; + if (prev) { + prev.xterm.clearBuffer(); + prev.xterm.clearSearchDecorations(); + // clearBuffer tries to retain the prompt line, but this doesn't exist for tests. + // So clear the screen (J) and move to home (H) to ensure previous data is cleaned up. + prev.xterm.write(`\x1b[2J\x1b[0;0H`); + return prev; + } + + const capabilities = new TerminalCapabilityStore(); + const cwd = this.terminalCwd; + capabilities.add(TerminalCapability.CwdDetection, { + type: TerminalCapability.CwdDetection, + get cwds() { return [cwd.value]; }, + onDidChangeCwd: cwd.onDidChange, + getCwd: () => cwd.value, + updateCwd: () => { }, + }); + + return this.terminal.value = await this.terminalService.createDetachedTerminal({ + rows: 10, + cols: 80, + readonly: true, + capabilities, + processInfo: new DetachedProcessInfo({ initialCwd: cwd.value }), + colorProvider: { + getBackgroundColor: theme => { + const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); + if (terminalBackground) { + return terminalBackground; + } + if (this.isInPeekView) { + return theme.getColor(peekViewResultsBackground); + } + const location = this.viewDescriptorService.getViewLocationById(Testing.ResultsViewId); + return location === ViewContainerLocation.Panel + ? theme.getColor(PANEL_BACKGROUND) + : theme.getColor(SIDE_BAR_BACKGROUND); + }, + } + }); + } + + public async update(subject: InspectSubject) { + this.outputDataListener.clear(); + if (subject instanceof TaskSubject) { + await this.updateForTaskSubject(subject); + } else if (subject instanceof TestOutputSubject || (subject instanceof MessageSubject && subject.message.type === TestMessageType.Output)) { + await this.updateForTestSubject(subject); + } else { + this.clear(); + } + } + + private async updateForTestSubject(subject: TestOutputSubject | MessageSubject) { + const that = this; + const testItem = subject instanceof TestOutputSubject ? subject.test.item : subject.test; + const terminal = await this.updateGenerically({ + subject, + noOutputMessage: localize('caseNoOutput', 'The test case did not report any output.'), + getTarget: result => result?.tasks[subject.taskIndex].output, + *doInitialWrite(output, results) { + that.updateCwd(testItem.uri); + const state = subject instanceof TestOutputSubject ? subject.test : results.getStateById(testItem.extId); + if (!state) { + return; + } + + for (const message of state.tasks[subject.taskIndex].messages) { + if (message.type === TestMessageType.Output) { + yield* output.getRangeIter(message.offset, message.length); + } + } + }, + doListenForMoreData: (output, result, write) => result.onChange(e => { + if (e.reason === TestResultItemChangeReason.NewMessage && e.item.item.extId === testItem.extId && e.message.type === TestMessageType.Output) { + for (const chunk of output.getRangeIter(e.message.offset, e.message.length)) { + write(chunk.buffer); + } + } + }), + }); + + if (subject instanceof MessageSubject && subject.message.type === TestMessageType.Output && subject.message.marker !== undefined) { + terminal?.xterm.selectMarkedRange(getMarkId(subject.message.marker, true), getMarkId(subject.message.marker, false), /* scrollIntoView= */ true); + } + } + + private updateForTaskSubject(subject: TaskSubject) { + return this.updateGenerically({ + subject, + noOutputMessage: localize('runNoOutput', 'The test run did not record any output.'), + getTarget: result => result?.tasks[subject.taskIndex], + doInitialWrite: (task, result) => { + // Update the cwd and use the first test to try to hint at the correct cwd, + // but often this will fall back to the first workspace folder. + this.updateCwd(Iterable.find(result.tests, t => !!t.item.uri)?.item.uri); + return task.output.buffers; + }, + doListenForMoreData: (task, _result, write) => task.output.onDidWriteData(e => write(e.buffer)), + }); + } + + private async updateGenerically(opts: { + subject: InspectSubject; + noOutputMessage: string; + getTarget: (result: ITestResult) => T | undefined; + doInitialWrite: (target: T, result: LiveTestResult) => Iterable; + doListenForMoreData: (target: T, result: LiveTestResult, write: (s: Uint8Array) => void) => IDisposable; + }) { + const result = opts.subject.result; + const target = opts.getTarget(result); + if (!target) { + return this.clear(); + } + + const terminal = await this.makeTerminal(); + let didWriteData = false; + + const pendingWrites = new MutableObservableValue(0); + if (result instanceof LiveTestResult) { + for (const chunk of opts.doInitialWrite(target, result)) { + didWriteData ||= chunk.byteLength > 0; + pendingWrites.value++; + terminal.xterm.write(chunk.buffer, () => pendingWrites.value--); + } + } else { + didWriteData = true; + this.writeNotice(terminal, localize('runNoOutputForPast', 'Test output is only available for new test runs.')); + } + + this.attachTerminalToDom(terminal); + this.outputDataListener.clear(); + + if (result instanceof LiveTestResult && !result.completedAt) { + const l1 = result.onComplete(() => { + if (!didWriteData) { + this.writeNotice(terminal, opts.noOutputMessage); + } + }); + const l2 = opts.doListenForMoreData(target, result, data => { + terminal.xterm.write(data); + didWriteData ||= data.byteLength > 0; + }); + + this.outputDataListener.value = combinedDisposable(l1, l2); + } + + if (!this.outputDataListener.value && !didWriteData) { + this.writeNotice(terminal, opts.noOutputMessage); + } + + // Ensure pending writes finish, otherwise the selection in `updateForTestSubject` + // can happen before the markers are processed. + if (pendingWrites.value > 0) { + await new Promise(resolve => { + const l = pendingWrites.onDidChange(() => { + if (pendingWrites.value === 0) { + l.dispose(); + resolve(); + } + }); + }); + } + + return terminal; + } + + private updateCwd(testUri?: URI) { + const wf = (testUri && this.workspaceContext.getWorkspaceFolder(testUri)) + || this.workspaceContext.getWorkspace().folders[0]; + if (wf) { + this.terminalCwd.value = wf.uri.fsPath; + } + } + + private writeNotice(terminal: IDetachedTerminalInstance, str: string) { + terminal.xterm.write(formatMessageForTerminal(str)); + } + + private attachTerminalToDom(terminal: IDetachedTerminalInstance) { + terminal.xterm.write('\x1b[?25l'); // hide cursor + dom.scheduleAtNextAnimationFrame(dom.getWindow(this.container), () => this.layoutTerminal(terminal)); + terminal.attachToElement(this.container, { enableGpu: false }); + } + + private clear() { + this.outputDataListener.clear(); + this.xtermLayoutDelayer.cancel(); + this.terminal.clear(); + } + + public layout(dimensions: dom.IDimension) { + this.dimensions = dimensions; + if (this.terminal.value) { + this.layoutTerminal(this.terminal.value, dimensions.width, dimensions.height); + } + } + + private layoutTerminal( + { xterm }: IDetachedTerminalInstance, + width = this.dimensions?.width ?? this.container.clientWidth, + height = this.dimensions?.height ?? this.container.clientHeight + ) { + width -= 10 + 20; // scrollbar width + margin + this.xtermLayoutDelayer.trigger(() => { + const scaled = getXtermScaledDimensions(dom.getWindow(this.container), xterm.getFont(), width, height); + if (scaled) { + xterm.resize(scaled.cols, scaled.rows); + } + }); + } +} + +const isMultiline = (str: string | undefined) => !!str && str.includes('\n'); diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.ts new file mode 100644 index 0000000000000..7fa9da1daa33f --- /dev/null +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject.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 { MarshalledId } from 'vs/base/common/marshallingIds'; +import { URI } from 'vs/base/common/uri'; +import { Range } from 'vs/editor/common/core/range'; +import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import { IRichLocation, ITestItem, ITestMessage, ITestMessageMenuArgs, ITestRunTask, ITestTaskState, InternalTestItem, TestMessageType, TestResultItem } from 'vs/workbench/contrib/testing/common/testTypes'; +import { TestUriType, buildTestUri } from 'vs/workbench/contrib/testing/common/testingUri'; + +export const getMessageArgs = (test: TestResultItem, message: ITestMessage): ITestMessageMenuArgs => ({ + $mid: MarshalledId.TestMessageMenuArgs, + test: InternalTestItem.serialize(test), + message: ITestMessage.serialize(message), +}); + +export class MessageSubject { + public readonly test: ITestItem; + public readonly message: ITestMessage; + public readonly expectedUri: URI; + public readonly actualUri: URI; + public readonly messageUri: URI; + public readonly revealLocation: IRichLocation | undefined; + public readonly context: ITestMessageMenuArgs | undefined; + + public get isDiffable() { + return this.message.type === TestMessageType.Error && ITestMessage.isDiffable(this.message); + } + + public get contextValue() { + return this.message.type === TestMessageType.Error ? this.message.contextValue : undefined; + } + + public get stack() { + return this.message.type === TestMessageType.Error && this.message.stackTrace?.length ? this.message.stackTrace : undefined; + } + + constructor(public readonly result: ITestResult, test: TestResultItem, public readonly taskIndex: number, public readonly messageIndex: number) { + this.test = test.item; + const messages = test.tasks[taskIndex].messages; + this.messageIndex = messageIndex; + + const parts = { messageIndex, resultId: result.id, taskIndex, testExtId: test.item.extId }; + this.expectedUri = buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }); + this.actualUri = buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }); + this.messageUri = buildTestUri({ ...parts, type: TestUriType.ResultMessage }); + + const message = this.message = messages[this.messageIndex]; + this.context = getMessageArgs(test, message); + this.revealLocation = message.location ?? (test.item.uri && test.item.range ? { uri: test.item.uri, range: Range.lift(test.item.range) } : undefined); + } +} + +export class TaskSubject { + public readonly outputUri: URI; + public readonly revealLocation: undefined; + + constructor(public readonly result: ITestResult, public readonly taskIndex: number) { + this.outputUri = buildTestUri({ resultId: result.id, taskIndex, type: TestUriType.TaskOutput }); + } +} + +export class TestOutputSubject { + public readonly outputUri: URI; + public readonly revealLocation: undefined; + public readonly task: ITestRunTask; + + constructor(public readonly result: ITestResult, public readonly taskIndex: number, public readonly test: TestResultItem) { + this.outputUri = buildTestUri({ resultId: this.result.id, taskIndex: this.taskIndex, testExtId: this.test.item.extId, type: TestUriType.TestOutput }); + this.task = result.tasks[this.taskIndex]; + } +} + +export type InspectSubject = MessageSubject | TaskSubject | TestOutputSubject; + +export const equalsSubject = (a: InspectSubject, b: InspectSubject) => ( + (a instanceof MessageSubject && b instanceof MessageSubject && a.message === b.message) || + (a instanceof TaskSubject && b instanceof TaskSubject && a.result === b.result && a.taskIndex === b.taskIndex) || + (a instanceof TestOutputSubject && b instanceof TestOutputSubject && a.test === b.test && a.taskIndex === b.taskIndex) +); + + +export const mapFindTestMessage = (test: TestResultItem, fn: (task: ITestTaskState, message: ITestMessage, messageIndex: number, taskIndex: number) => T | undefined) => { + for (let taskIndex = 0; taskIndex < test.tasks.length; taskIndex++) { + const task = test.tasks[taskIndex]; + for (let messageIndex = 0; messageIndex < task.messages.length; messageIndex++) { + const r = fn(task, task.messages[messageIndex], messageIndex, taskIndex); + if (r !== undefined) { + return r; + } + } + } + + return undefined; +}; diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts new file mode 100644 index 0000000000000..f6aa368c619fc --- /dev/null +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts @@ -0,0 +1,853 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; +import { ITreeContextMenuEvent, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { Action, IAction, Separator } from 'vs/base/common/actions'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { Iterable } from 'vs/base/common/iterator'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { autorun } from 'vs/base/common/observable'; +import { count } from 'vs/base/common/strings'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { isDefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { MenuEntryActionViewItem, createAndFillInActionBarActions } 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 { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IProgressService } from 'vs/platform/progress/common/progress'; +import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; +import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; +import * as icons from 'vs/workbench/contrib/testing/browser/icons'; +import { renderTestMessageAsText } from 'vs/workbench/contrib/testing/browser/testMessageColorizer'; +import { TestOutputSubject, InspectSubject, TaskSubject, MessageSubject, mapFindTestMessage, getMessageArgs } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject'; +import { Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; +import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; +import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; +import { ITestResult, ITestRunTaskResults, LiveTestResult, TestResultItemChangeReason, maxCountPriority } from 'vs/workbench/contrib/testing/common/testResult'; +import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; +import { IRichLocation, ITestItemContext, ITestMessage, ITestMessageMenuArgs, InternalTestItem, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, testResultStateToContextValues } from 'vs/workbench/contrib/testing/common/testTypes'; +import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { cmpPriority } from 'vs/workbench/contrib/testing/common/testingStates'; +import { TestUriType, buildTestUri } from 'vs/workbench/contrib/testing/common/testingUri'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + + +interface ITreeElement { + type: string; + context: unknown; + id: string; + label: string; + onDidChange: Event; + labelWithIcons?: readonly (HTMLSpanElement | string)[]; + icon?: ThemeIcon; + description?: string; + ariaLabel?: string; +} + +interface ITreeElement { + type: string; + context: unknown; + id: string; + label: string; + onDidChange: Event; + labelWithIcons?: readonly (HTMLSpanElement | string)[]; + icon?: ThemeIcon; + description?: string; + ariaLabel?: string; +} + +class TestResultElement implements ITreeElement { + public readonly changeEmitter = new Emitter(); + public readonly onDidChange = this.changeEmitter.event; + public readonly type = 'result'; + public readonly context = this.value.id; + public readonly id = this.value.id; + public readonly label = this.value.name; + + public get icon() { + return icons.testingStatesToIcons.get( + this.value.completedAt === undefined + ? TestResultState.Running + : maxCountPriority(this.value.counts) + ); + } + + constructor(public readonly value: ITestResult) { } +} + +const openCoverageLabel = localize('openTestCoverage', 'View Test Coverage'); +const closeCoverageLabel = localize('closeTestCoverage', 'Close Test Coverage'); + +class CoverageElement implements ITreeElement { + public readonly type = 'coverage'; + public readonly context: undefined; + public readonly id = `coverage-${this.results.id}/${this.task.id}`; + public readonly onDidChange: Event; + + public get label() { + return this.isOpen ? closeCoverageLabel : openCoverageLabel; + } + + public get icon() { + return this.isOpen ? widgetClose : icons.testingCoverageReport; + } + + public get isOpen() { + return this.coverageService.selected.get()?.fromTaskId === this.task.id; + } + + constructor( + private readonly results: ITestResult, + public readonly task: ITestRunTaskResults, + private readonly coverageService: ITestCoverageService, + ) { + this.onDidChange = Event.fromObservableLight(coverageService.selected); + } + +} + +class TestCaseElement implements ITreeElement { + public readonly type = 'test'; + public readonly context: ITestItemContext = { + $mid: MarshalledId.TestItemContext, + tests: [InternalTestItem.serialize(this.test)], + }; + public readonly id = `${this.results.id}/${this.test.item.extId}`; + public readonly description?: string; + + public get onDidChange() { + if (!(this.results instanceof LiveTestResult)) { + return Event.None; + } + + return Event.filter(this.results.onChange, e => e.item.item.extId === this.test.item.extId); + } + + public get state() { + return this.test.tasks[this.taskIndex].state; + } + + public get label() { + return this.test.item.label; + } + + public get labelWithIcons() { + return renderLabelWithIcons(this.label); + } + + public get icon() { + return icons.testingStatesToIcons.get(this.state); + } + + public get outputSubject() { + return new TestOutputSubject(this.results, this.taskIndex, this.test); + } + + + constructor( + public readonly results: ITestResult, + public readonly test: TestResultItem, + public readonly taskIndex: number, + ) { } +} + +class TaskElement implements ITreeElement { + public readonly changeEmitter = new Emitter(); + public readonly onDidChange = this.changeEmitter.event; + public readonly type = 'task'; + public readonly context: string; + public readonly id: string; + public readonly label: string; + public readonly itemsCache = new CreationCache(); + + public get icon() { + return this.results.tasks[this.index].running ? icons.testingStatesToIcons.get(TestResultState.Running) : undefined; + } + + constructor(public readonly results: ITestResult, public readonly task: ITestRunTaskResults, public readonly index: number) { + this.id = `${results.id}/${index}`; + this.task = results.tasks[index]; + this.context = String(index); + this.label = this.task.name ?? localize('testUnnamedTask', 'Unnamed Task'); + } +} + +class TestMessageElement implements ITreeElement { + public readonly type = 'message'; + public readonly id: string; + public readonly label: string; + public readonly uri: URI; + public readonly location?: IRichLocation; + public readonly description?: string; + public readonly contextValue?: string; + public readonly message: ITestMessage; + + public get onDidChange() { + if (!(this.result instanceof LiveTestResult)) { + return Event.None; + } + + // rerender when the test case changes so it gets retired events + return Event.filter(this.result.onChange, e => e.item.item.extId === this.test.item.extId); + } + + public get context(): ITestMessageMenuArgs { + return getMessageArgs(this.test, this.message); + } + + public get outputSubject() { + return new TestOutputSubject(this.result, this.taskIndex, this.test); + } + + constructor( + public readonly result: ITestResult, + public readonly test: TestResultItem, + public readonly taskIndex: number, + public readonly messageIndex: number, + ) { + const m = this.message = test.tasks[taskIndex].messages[messageIndex]; + + this.location = m.location; + this.contextValue = m.type === TestMessageType.Error ? m.contextValue : undefined; + this.uri = buildTestUri({ + type: TestUriType.ResultMessage, + messageIndex, + resultId: result.id, + taskIndex, + testExtId: test.item.extId + }); + + this.id = this.uri.toString(); + + const asPlaintext = renderTestMessageAsText(m.message); + const lines = count(asPlaintext.trimEnd(), '\n'); + this.label = firstLine(asPlaintext); + if (lines > 0) { + this.description = lines > 1 + ? localize('messageMoreLinesN', '+ {0} more lines', lines) + : localize('messageMoreLines1', '+ 1 more line'); + } + } +} + +type TreeElement = TestResultElement | TestCaseElement | TestMessageElement | TaskElement | CoverageElement; + +export class OutputPeekTree extends Disposable { + private disposed = false; + private readonly tree: WorkbenchCompressibleObjectTree; + private readonly treeActions: TreeActionsProvider; + private readonly requestReveal = this._register(new Emitter()); + + public readonly onDidRequestReview = this.requestReveal.event; + + constructor( + container: HTMLElement, + onDidReveal: Event<{ subject: InspectSubject; preserveFocus: boolean }>, + options: { showRevealLocationOnMessages: boolean; locationForProgress: string }, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @ITestResultService results: ITestResultService, + @IInstantiationService instantiationService: IInstantiationService, + @ITestExplorerFilterState explorerFilter: ITestExplorerFilterState, + @ITestCoverageService coverageService: ITestCoverageService, + @IProgressService progressService: IProgressService, + ) { + super(); + + this.treeActions = instantiationService.createInstance(TreeActionsProvider, options.showRevealLocationOnMessages, this.requestReveal,); + const diffIdentityProvider: IIdentityProvider = { + getId(e: TreeElement) { + return e.id; + } + }; + + this.tree = this._register(instantiationService.createInstance( + WorkbenchCompressibleObjectTree, + 'Test Output Peek', + container, + { + getHeight: () => 22, + getTemplateId: () => TestRunElementRenderer.ID, + }, + [instantiationService.createInstance(TestRunElementRenderer, this.treeActions)], + { + compressionEnabled: true, + hideTwistiesOfChildlessElements: true, + identityProvider: diffIdentityProvider, + sorter: { + compare(a, b) { + if (a instanceof TestCaseElement && b instanceof TestCaseElement) { + return cmpPriority(a.state, b.state); + } + + return 0; + }, + }, + accessibilityProvider: { + getAriaLabel(element: ITreeElement) { + return element.ariaLabel || element.label; + }, + getWidgetAriaLabel() { + return localize('testingPeekLabel', 'Test Result Messages'); + } + } + }, + )) as WorkbenchCompressibleObjectTree; + + const cc = new CreationCache(); + const getTaskChildren = (taskElem: TaskElement): Iterable> => { + const { results, index, itemsCache, task } = taskElem; + const tests = Iterable.filter(results.tests, test => test.tasks[index].state >= TestResultState.Running || test.tasks[index].messages.length > 0); + let result: Iterable> = Iterable.map(tests, test => ({ + element: itemsCache.getOrCreate(test, () => new TestCaseElement(results, test, index)), + incompressible: true, + children: getTestChildren(results, test, index), + })); + + if (task.coverage.get()) { + result = Iterable.concat( + Iterable.single>({ + element: new CoverageElement(results, task, coverageService), + collapsible: true, + incompressible: true, + }), + result, + ); + } + + return result; + }; + + const getTestChildren = (result: ITestResult, test: TestResultItem, taskIndex: number): Iterable> => { + return test.tasks[taskIndex].messages + .map((m, messageIndex) => + m.type === TestMessageType.Error + ? { element: cc.getOrCreate(m, () => new TestMessageElement(result, test, taskIndex, messageIndex)), incompressible: false } + : undefined + ) + .filter(isDefined); + }; + + const getResultChildren = (result: ITestResult): Iterable> => { + return result.tasks.map((task, taskIndex) => { + const taskElem = cc.getOrCreate(task, () => new TaskElement(result, task, taskIndex)); + return ({ + element: taskElem, + incompressible: false, + collapsible: true, + children: getTaskChildren(taskElem), + }); + }); + }; + + const getRootChildren = () => results.results.map(result => { + const element = cc.getOrCreate(result, () => new TestResultElement(result)); + return { + element, + incompressible: true, + collapsible: true, + collapsed: this.tree.hasElement(element) ? this.tree.isCollapsed(element) : true, + children: getResultChildren(result) + }; + }); + + // Queued result updates to prevent spamming CPU when lots of tests are + // completing and messaging quickly (#142514) + const taskChildrenToUpdate = new Set(); + const taskChildrenUpdate = this._register(new RunOnceScheduler(() => { + for (const taskNode of taskChildrenToUpdate) { + if (this.tree.hasElement(taskNode)) { + this.tree.setChildren(taskNode, getTaskChildren(taskNode), { diffIdentityProvider }); + } + } + taskChildrenToUpdate.clear(); + }, 300)); + + const queueTaskChildrenUpdate = (taskNode: TaskElement) => { + taskChildrenToUpdate.add(taskNode); + if (!taskChildrenUpdate.isScheduled()) { + taskChildrenUpdate.schedule(); + } + }; + + const attachToResults = (result: LiveTestResult) => { + const resultNode = cc.get(result)! as TestResultElement; + const disposable = new DisposableStore(); + disposable.add(result.onNewTask(i => { + if (result.tasks.length === 1) { + this.requestReveal.fire(new TaskSubject(result, 0)); // reveal the first task in new runs + } + + if (this.tree.hasElement(resultNode)) { + this.tree.setChildren(resultNode, getResultChildren(result), { diffIdentityProvider }); + } + + // note: tasks are bounded and their lifetime is equivalent to that of + // the test result, so this doesn't leak indefinitely. + const task = result.tasks[i]; + disposable.add(autorun(reader => { + task.coverage.read(reader); // add it to the autorun + queueTaskChildrenUpdate(cc.get(task) as TaskElement); + })); + })); + disposable.add(result.onEndTask(index => { + (cc.get(result.tasks[index]) as TaskElement | undefined)?.changeEmitter.fire(); + })); + + disposable.add(result.onChange(e => { + // try updating the item in each of its tasks + for (const [index, task] of result.tasks.entries()) { + const taskNode = cc.get(task) as TaskElement; + if (!this.tree.hasElement(taskNode)) { + continue; + } + + const itemNode = taskNode.itemsCache.get(e.item); + if (itemNode && this.tree.hasElement(itemNode)) { + if (e.reason === TestResultItemChangeReason.NewMessage && e.message.type === TestMessageType.Error) { + this.tree.setChildren(itemNode, getTestChildren(result, e.item, index), { diffIdentityProvider }); + } + return; + } + + queueTaskChildrenUpdate(taskNode); + } + })); + + disposable.add(result.onComplete(() => { + resultNode.changeEmitter.fire(); + disposable.dispose(); + })); + + return resultNode; + }; + + this._register(results.onResultsChanged(e => { + // little hack here: a result change can cause the peek to be disposed, + // but this listener will still be queued. Doing stuff with the tree + // will cause errors. + if (this.disposed) { + return; + } + + if ('completed' in e) { + (cc.get(e.completed) as TestResultElement | undefined)?.changeEmitter.fire(); + return; + } + + this.tree.setChildren(null, getRootChildren(), { diffIdentityProvider }); + + // done after setChildren intentionally so that the ResultElement exists in the cache. + if ('started' in e) { + for (const child of this.tree.getNode(null).children) { + this.tree.collapse(child.element, false); + } + + this.tree.expand(attachToResults(e.started), true); + } + })); + + const revealItem = (element: TreeElement, preserveFocus: boolean) => { + this.tree.setFocus([element]); + this.tree.setSelection([element]); + if (!preserveFocus) { + this.tree.domFocus(); + } + }; + + this._register(onDidReveal(async ({ subject, preserveFocus = false }) => { + if (subject instanceof TaskSubject) { + const resultItem = this.tree.getNode(null).children.find(c => { + if (c.element instanceof TaskElement) { + return c.element.results.id === subject.result.id && c.element.index === subject.taskIndex; + } + if (c.element instanceof TestResultElement) { + return c.element.id === subject.result.id; + } + return false; + }); + + if (resultItem) { + revealItem(resultItem.element!, preserveFocus); + } + return; + } + + const revealElement = subject instanceof TestOutputSubject + ? cc.get(subject.task)?.itemsCache.get(subject.test) + : cc.get(subject.message); + if (!revealElement || !this.tree.hasElement(revealElement)) { + return; + } + + const parents: TreeElement[] = []; + for (let parent = this.tree.getParentElement(revealElement); parent; parent = this.tree.getParentElement(parent)) { + parents.unshift(parent); + } + + for (const parent of parents) { + this.tree.expand(parent); + } + + if (this.tree.getRelativeTop(revealElement) === null) { + this.tree.reveal(revealElement, 0.5); + } + + revealItem(revealElement, preserveFocus); + })); + + this._register(this.tree.onDidOpen(async e => { + if (e.element instanceof TestMessageElement) { + this.requestReveal.fire(new MessageSubject(e.element.result, e.element.test, e.element.taskIndex, e.element.messageIndex)); + } else if (e.element instanceof TestCaseElement) { + const t = e.element; + const message = mapFindTestMessage(e.element.test, (_t, _m, mesasgeIndex, taskIndex) => + new MessageSubject(t.results, t.test, taskIndex, mesasgeIndex)); + this.requestReveal.fire(message || new TestOutputSubject(t.results, 0, t.test)); + } else if (e.element instanceof CoverageElement) { + const task = e.element.task; + if (e.element.isOpen) { + return coverageService.closeCoverage(); + } + progressService.withProgress( + { location: options.locationForProgress }, + () => coverageService.openCoverage(task, true) + ); + } + })); + + this._register(this.tree.onDidChangeSelection(evt => { + for (const element of evt.elements) { + if (element && 'test' in element) { + explorerFilter.reveal.value = element.test.item.extId; + break; + } + } + })); + + + this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); + + this.tree.setChildren(null, getRootChildren()); + for (const result of results.results) { + if (!result.completedAt && result instanceof LiveTestResult) { + attachToResults(result); + } + } + } + + public layout(height: number, width: number) { + this.tree.layout(height, width); + } + + private onContextMenu(evt: ITreeContextMenuEvent) { + if (!evt.element) { + return; + } + + const actions = this.treeActions.provideActionBar(evt.element); + this.contextMenuService.showContextMenu({ + getAnchor: () => evt.anchor, + getActions: () => actions.secondary.length + ? [...actions.primary, new Separator(), ...actions.secondary] + : actions.primary, + getActionsContext: () => evt.element?.context + }); + } + + public override dispose() { + super.dispose(); + this.disposed = true; + } +} + +interface TemplateData { + label: HTMLElement; + icon: HTMLElement; + actionBar: ActionBar; + elementDisposable: DisposableStore; + templateDisposable: DisposableStore; +} + +class TestRunElementRenderer implements ICompressibleTreeRenderer { + public static readonly ID = 'testRunElementRenderer'; + public readonly templateId = TestRunElementRenderer.ID; + + constructor( + private readonly treeActions: TreeActionsProvider, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + /** @inheritdoc */ + public renderCompressedElements(node: ITreeNode, FuzzyScore>, _index: number, templateData: TemplateData): void { + const chain = node.element.elements; + const lastElement = chain[chain.length - 1]; + if ((lastElement instanceof TaskElement || lastElement instanceof TestMessageElement) && chain.length >= 2) { + this.doRender(chain[chain.length - 2], templateData, lastElement); + } else { + this.doRender(lastElement, templateData); + } + } + + /** @inheritdoc */ + public renderTemplate(container: HTMLElement): TemplateData { + const templateDisposable = new DisposableStore(); + const wrapper = dom.append(container, dom.$('.test-peek-item')); + const icon = dom.append(wrapper, dom.$('.state')); + const label = dom.append(wrapper, dom.$('.name')); + + const actionBar = new ActionBar(wrapper, { + actionViewItemProvider: (action, options) => + action instanceof MenuItemAction + ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }) + : undefined + }); + + const elementDisposable = new DisposableStore(); + templateDisposable.add(elementDisposable); + templateDisposable.add(actionBar); + + return { + icon, + label, + actionBar, + elementDisposable, + templateDisposable, + }; + } + + /** @inheritdoc */ + public renderElement(element: ITreeNode, _index: number, templateData: TemplateData): void { + this.doRender(element.element, templateData); + } + + /** @inheritdoc */ + public disposeTemplate(templateData: TemplateData): void { + templateData.templateDisposable.dispose(); + } + + /** Called to render a new element */ + private doRender(element: ITreeElement, templateData: TemplateData, subjectElement?: ITreeElement) { + templateData.elementDisposable.clear(); + templateData.elementDisposable.add( + element.onDidChange(() => this.doRender(element, templateData, subjectElement)), + ); + this.doRenderInner(element, templateData, subjectElement); + } + + /** Called, and may be re-called, to render or re-render an element */ + private doRenderInner(element: ITreeElement, templateData: TemplateData, subjectElement: ITreeElement | undefined) { + let { label, labelWithIcons, description } = element; + if (subjectElement instanceof TestMessageElement) { + description = subjectElement.label; + } + + const descriptionElement = description ? dom.$('span.test-label-description', {}, description) : ''; + if (labelWithIcons) { + dom.reset(templateData.label, ...labelWithIcons, descriptionElement); + } else { + dom.reset(templateData.label, label, descriptionElement); + } + + const icon = element.icon; + templateData.icon.className = `computed-state ${icon ? ThemeIcon.asClassName(icon) : ''}`; + + const actions = this.treeActions.provideActionBar(element); + templateData.actionBar.clear(); + templateData.actionBar.context = element.context; + templateData.actionBar.push(actions.primary, { icon: true, label: false }); + } +} + +class TreeActionsProvider { + constructor( + private readonly showRevealLocationOnMessages: boolean, + private readonly requestReveal: Emitter, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + @ICommandService private readonly commandService: ICommandService, + @ITestProfileService private readonly testProfileService: ITestProfileService, + @IEditorService private readonly editorService: IEditorService, + ) { } + + public provideActionBar(element: ITreeElement) { + const test = element instanceof TestCaseElement ? element.test : undefined; + const capabilities = test ? this.testProfileService.capabilitiesForTest(test) : 0; + + const contextKeys: [string, unknown][] = [ + ['peek', Testing.OutputPeekContributionId], + [TestingContextKeys.peekItemType.key, element.type], + ]; + + let id = MenuId.TestPeekElement; + const primary: IAction[] = []; + const secondary: IAction[] = []; + + if (element instanceof TaskElement) { + primary.push(new Action( + 'testing.outputPeek.showResultOutput', + localize('testing.showResultOutput', "Show Result Output"), + ThemeIcon.asClassName(Codicon.terminal), + undefined, + () => this.requestReveal.fire(new TaskSubject(element.results, element.index)), + )); + } + + if (element instanceof TestResultElement) { + // only show if there are no collapsed test nodes that have more specific choices + if (element.value.tasks.length === 1) { + primary.push(new Action( + 'testing.outputPeek.showResultOutput', + localize('testing.showResultOutput', "Show Result Output"), + ThemeIcon.asClassName(Codicon.terminal), + undefined, + () => this.requestReveal.fire(new TaskSubject(element.value, 0)), + )); + } + + primary.push(new Action( + 'testing.outputPeek.reRunLastRun', + localize('testing.reRunLastRun', "Rerun Test Run"), + ThemeIcon.asClassName(icons.testingRunIcon), + undefined, + () => this.commandService.executeCommand('testing.reRunLastRun', element.value.id), + )); + + if (capabilities & TestRunProfileBitset.Debug) { + primary.push(new Action( + 'testing.outputPeek.debugLastRun', + localize('testing.debugLastRun', "Debug Test Run"), + ThemeIcon.asClassName(icons.testingDebugIcon), + undefined, + () => this.commandService.executeCommand('testing.debugLastRun', element.value.id), + )); + } + } + + if (element instanceof TestCaseElement || element instanceof TestMessageElement) { + contextKeys.push( + [TestingContextKeys.testResultOutdated.key, element.test.retired], + [TestingContextKeys.testResultState.key, testResultStateToContextValues[element.test.ownComputedState]], + ...getTestItemContextOverlay(element.test, capabilities), + ); + + const extId = element.test.item.extId; + if (element.test.tasks[element.taskIndex].messages.some(m => m.type === TestMessageType.Output)) { + primary.push(new Action( + 'testing.outputPeek.showResultOutput', + localize('testing.showResultOutput', "Show Result Output"), + ThemeIcon.asClassName(Codicon.terminal), + undefined, + () => this.requestReveal.fire(element.outputSubject), + )); + } + + secondary.push(new Action( + 'testing.outputPeek.revealInExplorer', + localize('testing.revealInExplorer', "Reveal in Test Explorer"), + ThemeIcon.asClassName(Codicon.listTree), + undefined, + () => this.commandService.executeCommand('_revealTestInExplorer', extId), + )); + + if (capabilities & TestRunProfileBitset.Run) { + primary.push(new Action( + 'testing.outputPeek.runTest', + localize('run test', 'Run Test'), + ThemeIcon.asClassName(icons.testingRunIcon), + undefined, + () => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Run, extId), + )); + } + + if (capabilities & TestRunProfileBitset.Debug) { + primary.push(new Action( + 'testing.outputPeek.debugTest', + localize('debug test', 'Debug Test'), + ThemeIcon.asClassName(icons.testingDebugIcon), + undefined, + () => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Debug, extId), + )); + } + + } + + if (element instanceof TestMessageElement) { + primary.push(new Action( + 'testing.outputPeek.goToFile', + localize('testing.goToFile', "Go to Source"), + ThemeIcon.asClassName(Codicon.goToFile), + undefined, + () => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId), + )); + } + + if (element instanceof TestMessageElement) { + id = MenuId.TestMessageContext; + contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]); + if (this.showRevealLocationOnMessages && element.location) { + primary.push(new Action( + 'testing.outputPeek.goToError', + localize('testing.goToError', "Go to Source"), + ThemeIcon.asClassName(Codicon.goToFile), + undefined, + () => this.editorService.openEditor({ + resource: element.location!.uri, + options: { + selection: element.location!.range, + preserveFocus: true, + } + }), + )); + } + } + + + const contextOverlay = this.contextKeyService.createOverlay(contextKeys); + const result = { primary, secondary }; + const menu = this.menuService.getMenuActions(id, contextOverlay, { arg: element.context }); + createAndFillInActionBarActions(menu, result, 'inline'); + return result; + } +} + +class CreationCache { + private readonly v = new WeakMap(); + + public get(key: object): T2 | undefined { + return this.v.get(key) as T2 | undefined; + } + + public getOrCreate(ref: object, factory: () => T2): T2 { + const existing = this.v.get(ref); + if (existing) { + return existing as T2; + } + + const fresh = factory(); + this.v.set(ref, fresh); + return fresh; + } +} + +const firstLine = (str: string) => { + const index = str.indexOf('\n'); + return index === -1 ? str : str.slice(0, index); +}; diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.css b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.css similarity index 100% rename from src/vs/workbench/contrib/testing/browser/testingOutputPeek.css rename to src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.css diff --git a/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts new file mode 100644 index 0000000000000..e0dbd380fc818 --- /dev/null +++ b/src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts @@ -0,0 +1,388 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; +import { Limiter } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event, Relay } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import 'vs/css!./testResultsViewContent'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { FloatingClickMenu } from 'vs/platform/actions/browser/floatingMenu'; +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 { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { TestResultStackWidget } from 'vs/workbench/contrib/testing/browser/testResultsView/testMessageStack'; +import { DiffContentProvider, IPeekOutputRenderer, MarkdownTestMessagePeek, PlainTextMessagePeek, TerminalMessagePeek } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput'; +import { InspectSubject, MessageSubject, equalsSubject } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject'; +import { OutputPeekTree } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsTree'; +import { IObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; +import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import { ITestFollowup, ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { ITestMessageStackFrame } from 'vs/workbench/contrib/testing/common/testTypes'; +import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; +import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; + +const enum SubView { + CallStack = 0, + Diff = 1, + History = 2, +} + +/** UI state that can be saved/restored, used to give a nice experience when switching stack frames */ +export interface ITestResultsViewContentUiState { + splitViewWidths: number[]; +} + +export class TestResultsViewContent extends Disposable { + private static lastSplitWidth?: number; + + private readonly didReveal = this._register(new Emitter<{ subject: InspectSubject; preserveFocus: boolean }>()); + private readonly currentSubjectStore = this._register(new DisposableStore()); + private readonly onCloseEmitter = this._register(new Relay()); + private readonly onDidChangeStackFrameEmitter = this._register(new Relay()); + private followupWidget!: FollowupActionWidget; + private messageContextKeyService!: IContextKeyService; + private contextKeyTestMessage!: IContextKey; + private contextKeyResultOutdated!: IContextKey; + private callStackEl!: HTMLElement; + private readonly callStackWidget = this._register(new MutableDisposable()); + + private dimension?: dom.Dimension; + private splitView!: SplitView; + private messageContainer!: HTMLElement; + private contentProviders!: IPeekOutputRenderer[]; + private contentProvidersUpdateLimiter = this._register(new Limiter(1)); + + public current?: InspectSubject; + + /** Fired when a tree item is selected. Populated only on .fillBody() */ + public onDidRequestReveal!: Event; + + public readonly onClose = this.onCloseEmitter.event; + public readonly onDidChangeStackFrame = this.onDidChangeStackFrameEmitter.event; + + public get uiState(): ITestResultsViewContentUiState { + return { + splitViewWidths: Array.from( + { length: this.splitView.length }, + (_, i) => this.splitView.getViewSize(i) + ), + }; + } + + constructor( + private readonly editor: ICodeEditor | undefined, + private readonly options: { + historyVisible: IObservableValue; + showRevealLocationOnMessages: boolean; + locationForProgress: string; + }, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ITextModelService protected readonly modelService: ITextModelService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ITestingPeekOpener private readonly peekOpener: ITestingPeekOpener, + ) { + super(); + } + + public fillBody(containerElement: HTMLElement): void { + const initialSpitWidth = TestResultsViewContent.lastSplitWidth; + this.splitView = new SplitView(containerElement, { orientation: Orientation.HORIZONTAL }); + this.callStackEl = dom.append(containerElement, dom.$('.test-output-call-stack')); + + 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.editor)); + this.onCloseEmitter.input = this.followupWidget.onClose; + + this.contentProviders = [ + this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)), + this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)), + this._register(this.instantiationService.createInstance(TerminalMessagePeek, messageContainer, isInPeekView)), + this._register(this.instantiationService.createInstance(PlainTextMessagePeek, this.editor, messageContainer)), + ]; + + this._register(this.peekOpener.callStackVisible.onDidChange(() => { + if (this.current) { + this.updateVisiblityOfStackView(this.current); + } + })); + + this.messageContextKeyService = this._register(this.contextKeyService.createScoped(containerElement)); + this.contextKeyTestMessage = TestingContextKeys.testMessageContext.bindTo(this.messageContextKeyService); + this.contextKeyResultOutdated = TestingContextKeys.testResultOutdated.bindTo(this.messageContextKeyService); + + const treeContainer = dom.append(containerElement, dom.$('.test-output-peek-tree')); + const tree = this._register(this.instantiationService.createInstance( + OutputPeekTree, + treeContainer, + this.didReveal.event, + { showRevealLocationOnMessages, locationForProgress: this.options.locationForProgress }, + )); + + this.onDidRequestReveal = tree.onDidRequestReview; + + this.splitView.addView({ + onDidChange: Event.None, + element: messageContainer, + minimumSize: 200, + maximumSize: Number.MAX_VALUE, + layout: width => { + TestResultsViewContent.lastSplitWidth = width; + if (this.dimension) { + for (const provider of this.contentProviders) { + provider.layout({ height: this.dimension.height, width }); + } + } + }, + }, Sizing.Distribute); + + this.splitView.addView({ + onDidChange: Event.None, + element: treeContainer, + minimumSize: 100, + maximumSize: Number.MAX_VALUE, + layout: width => { + if (this.dimension) { + tree.layout(this.dimension.height, width); + } + }, + }, Sizing.Distribute); + + + this.splitView.setViewVisible(this.viewIndex(SubView.History), historyVisible.value); + this._register(historyVisible.onDidChange(visible => { + this.splitView.setViewVisible(this.viewIndex(SubView.History), visible); + })); + + if (initialSpitWidth) { + queueMicrotask(() => this.splitView.resizeView(0, initialSpitWidth)); + } + } + + /** + * Shows a message in-place without showing or changing the peek location. + * This is mostly used if peeking a message without a location. + */ + public reveal(opts: { + subject: InspectSubject; + preserveFocus: boolean; + frame?: ITestMessageStackFrame; + uiState?: ITestResultsViewContentUiState; + }) { + this.didReveal.fire(opts); + + if (this.current && equalsSubject(this.current, opts.subject)) { + return Promise.resolve(); + } + + this.current = opts.subject; + return this.contentProvidersUpdateLimiter.queue(async () => { + await Promise.all(this.contentProviders.map(p => p.update(opts.subject))); + this.followupWidget.show(opts.subject); + this.currentSubjectStore.clear(); + this.updateVisiblityOfStackView(opts.subject, opts.frame); + this.populateFloatingClick(opts.subject); + + if (opts.uiState) { + opts.uiState.splitViewWidths.forEach((width, i) => this.splitView.resizeView(i, width)); + } + }); + } + + private updateVisiblityOfStackView(subject: InspectSubject, frame?: ITestMessageStackFrame) { + const stack = this.peekOpener.callStackVisible.value && subject instanceof MessageSubject && subject.stack; + + if (stack) { + if (!this.callStackWidget.value) { + const widget = this.callStackWidget.value = this.instantiationService.createInstance(TestResultStackWidget, this.callStackEl); + this.splitView.addView({ + onDidChange: Event.None, + element: this.callStackEl, + minimumSize: 100, + maximumSize: Number.MAX_VALUE, + layout: width => widget.layout(undefined, width), + }, 150, 0); + this.onDidChangeStackFrameEmitter.input = widget.onDidChangeStackFrame; + } + + this.callStackWidget.value.update(stack, frame); + } else if (this.callStackWidget.value) { + this.splitView.removeView(0); + this.onDidChangeStackFrameEmitter.input = Event.None; + this.callStackWidget.clear(); + } + } + + private viewIndex(subView: SubView) { + // the call stack view is index 0, if it's not visible then all indicies are shifted by one + if (!this.callStackWidget.value) { + return subView - 1; + } + + return subView; + } + + private populateFloatingClick(subject: InspectSubject) { + if (!(subject instanceof MessageSubject)) { + return; + } + + this.currentSubjectStore.add(toDisposable(() => { + this.contextKeyResultOutdated.reset(); + this.contextKeyTestMessage.reset(); + })); + + this.contextKeyTestMessage.set(subject.contextValue || ''); + if (subject.result instanceof LiveTestResult) { + this.contextKeyResultOutdated.set(subject.result.getStateById(subject.test.extId)?.retired ?? false); + this.currentSubjectStore.add(subject.result.onChange(ev => { + if (ev.item.item.extId === subject.test.extId) { + this.contextKeyResultOutdated.set(ev.item.retired ?? false); + } + })); + } else { + this.contextKeyResultOutdated.set(true); + } + + 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) { + this.dimension = new dom.Dimension(width, height); + this.splitView.layout(width); + } + + public onWidth(width: number) { + this.splitView.layout(width); + } +} + +const FOLLOWUP_ANIMATION_MIN_TIME = 500; + +class FollowupActionWidget extends Disposable { + private readonly el = dom.h('div.testing-followup-action', []); + private readonly visibleStore = this._register(new DisposableStore()); + private readonly onCloseEmitter = this._register(new Emitter()); + public readonly onClose = this.onCloseEmitter.event; + + constructor( + private readonly container: HTMLElement, + private readonly editor: ICodeEditor | undefined, + @ITestService private readonly testService: ITestService, + @IQuickInputService private readonly quickInput: IQuickInputService, + ) { + super(); + } + + public show(subject: InspectSubject) { + this.visibleStore.clear(); + if (subject instanceof MessageSubject) { + this.showMessage(subject); + } + } + + private async showMessage(subject: MessageSubject) { + const cts = this.visibleStore.add(new CancellationTokenSource()); + const start = Date.now(); + + // Wait for completion otherwise results will not be available to the ext host: + if (subject.result instanceof LiveTestResult && !subject.result.completedAt) { + await new Promise(r => Event.once((subject.result as LiveTestResult).onComplete)(r)); + } + + const followups = await this.testService.provideTestFollowups({ + extId: subject.test.extId, + messageIndex: subject.messageIndex, + resultId: subject.result.id, + taskIndex: subject.taskIndex, + }, cts.token); + + + if (!followups.followups.length || cts.token.isCancellationRequested) { + followups.dispose(); + return; + } + + this.visibleStore.add(followups); + + dom.clearNode(this.el.root); + this.el.root.classList.toggle('animated', Date.now() - start > FOLLOWUP_ANIMATION_MIN_TIME); + + this.el.root.appendChild(this.makeFollowupLink(followups.followups[0])); + if (followups.followups.length > 1) { + this.el.root.appendChild(this.makeMoreLink(followups.followups)); + } + + this.container.appendChild(this.el.root); + this.visibleStore.add(toDisposable(() => { + this.el.root.remove(); + })); + } + + private makeFollowupLink(first: ITestFollowup) { + const link = this.makeLink(() => this.actionFollowup(link, first)); + dom.reset(link, ...renderLabelWithIcons(first.message)); + return link; + } + + private makeMoreLink(followups: ITestFollowup[]) { + const link = this.makeLink(() => + this.quickInput.pick(followups.map((f, i) => ({ + label: f.message, + index: i + }))).then(picked => { + if (picked?.length) { + followups[picked[0].index].execute(); + } + }) + ); + + link.innerText = localize('testFollowup.more', '+{0} More...', followups.length - 1); + return link; + } + + private makeLink(onClick: () => void) { + const link = document.createElement('a'); + link.tabIndex = 0; + this.visibleStore.add(dom.addDisposableListener(link, 'click', onClick)); + this.visibleStore.add(dom.addDisposableListener(link, 'keydown', e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + onClick(); + } + })); + + return link; + } + + private actionFollowup(link: HTMLAnchorElement, fu: ITestFollowup) { + if (link.ariaDisabled !== 'true') { + link.ariaDisabled = 'true'; + fu.execute(); + + if (this.editor) { + this.onCloseEmitter.fire(); + } + } + } +} diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index e422256e5197a..7df22eeebcb93 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -25,7 +25,7 @@ import { testingResultsIcon, testingViewIcon } from 'vs/workbench/contrib/testin import { TestCoverageView } from 'vs/workbench/contrib/testing/browser/testCoverageView'; import { TestingDecorationService, TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations'; import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; -import { CloseTestPeek, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestResultsView, TestingOutputPeekController, TestingPeekOpener, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; +import { CloseTestPeek, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestResultsView, TestingOutputPeekController, TestingPeekOpener, ToggleCallStackAction, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { TestingProgressTrigger } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { testingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; @@ -136,6 +136,7 @@ registerAction2(GoToPreviousMessageAction); registerAction2(GoToNextMessageAction); registerAction2(CloseTestPeek); registerAction2(ToggleTestingPeekHistory); +registerAction2(ToggleCallStackAction); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingPeekOpener, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index b2742e2480dc2..80f6674801a3d 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 6f90138b1022a..b4a2281c2155c 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 5c5572d1dc0a0..7bf8db331eb11 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; } + }); - if (this.filterState.isFilteringFor(TestFilterTerm.CurrentDoc)) { - this.tree.refilter(); + 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)); } - }; - this._register(editorService.onDidActiveEditorChange(onEditorChange)); + if (this.filterState.isFilteringFor(TestFilterTerm.CurrentDoc) || this.filterState.isFilteringFor(TestFilterTerm.OpenedFiles)) { + this.tree.refilter(); + } + })); 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 a05b7d50fe062..eefe1586d3cc0 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -4,57 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert } from 'vs/base/browser/ui/aria/aria'; -import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; -import { ICompressedTreeElement, ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; -import { ITreeContextMenuEvent, ITreeNode } from 'vs/base/browser/ui/tree/tree'; -import { Action, IAction, Separator } from 'vs/base/common/actions'; -import { Delayer, Limiter, RunOnceScheduler } from 'vs/base/common/async'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; -import { FuzzyScore } from 'vs/base/common/filters'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; import { stripIcons } from 'vs/base/common/iconLabels'; import { Iterable } from 'vs/base/common/iterator'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { MarshalledId } from 'vs/base/common/marshallingIds'; -import { autorun } from 'vs/base/common/observable'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { count } from 'vs/base/common/strings'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import 'vs/css!./testingOutputPeek'; -import { ICodeEditor, IDiffEditorConstructionOptions, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; -import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; -import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditor, IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IPeekViewService, PeekViewWidget, peekViewResultsBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/browser/peekView'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IPeekViewService, PeekViewWidget, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/browser/peekView'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { FloatingClickMenu } from 'vs/platform/actions/browser/floatingMenu'; -import { MenuEntryActionViewItem, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { Action2, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { Action2, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -65,116 +41,34 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IProgressService } from 'vs/platform/progress/common/progress'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; -import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; -import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { EditorModel } from 'vs/workbench/common/editor/editorModel'; -import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { DetachedProcessInfo } from 'vs/workbench/contrib/terminal/browser/detachedTerminal'; -import { IDetachedTerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { getXtermScaledDimensions } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; -import { TERMINAL_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; -import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; -import * as icons from 'vs/workbench/contrib/testing/browser/icons'; -import { colorizeTestMessageInEditor, renderTestMessageAsText } from 'vs/workbench/contrib/testing/browser/testMessageColorizer'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { renderTestMessageAsText } from 'vs/workbench/contrib/testing/browser/testMessageColorizer'; +import { InspectSubject, MessageSubject, TaskSubject, TestOutputSubject, mapFindTestMessage } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsSubject'; +import { ITestResultsViewContentUiState, TestResultsViewContent } from 'vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent'; import { testingMessagePeekBorder, testingPeekBorder, testingPeekHeaderBackground, testingPeekMessageHeaderBackground } from 'vs/workbench/contrib/testing/browser/theme'; import { AutoOpenPeekViewWhen, TestingConfigKeys, getTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { IObservableValue, MutableObservableValue, staticObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; +import { MutableObservableValue, staticObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; -import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; -import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; -import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; -import { ITaskRawOutput, ITestResult, ITestRunTaskResults, LiveTestResult, TestResultItemChange, TestResultItemChangeReason, maxCountPriority, resultItemParents } from 'vs/workbench/contrib/testing/common/testResult'; +import { ITestResult, TestResultItemChange, TestResultItemChangeReason, resultItemParents } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService'; -import { ITestFollowup, ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { IRichLocation, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessage, ITestMessageMenuArgs, ITestRunTask, ITestTaskState, InternalTestItem, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset, getMarkId, testResultStateToContextValues } from 'vs/workbench/contrib/testing/common/testTypes'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { IRichLocation, ITestMessage, ITestMessageStackFrame, TestMessageType, TestResultItem } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { IShowResultOptions, ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; -import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; +import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { ParsedTestUri, TestUriType, buildTestUri, parseTestUri } from 'vs/workbench/contrib/testing/common/testingUri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -const getMessageArgs = (test: TestResultItem, message: ITestMessage): ITestMessageMenuArgs => ({ - $mid: MarshalledId.TestMessageMenuArgs, - test: InternalTestItem.serialize(test), - message: ITestMessage.serialize(message), -}); - -class MessageSubject { - public readonly test: ITestItem; - public readonly message: ITestMessage; - public readonly expectedUri: URI; - public readonly actualUri: URI; - public readonly messageUri: URI; - public readonly revealLocation: IRichLocation | undefined; - public readonly context: ITestMessageMenuArgs | undefined; - - public get isDiffable() { - return this.message.type === TestMessageType.Error && isDiffable(this.message); - } - - public get contextValue() { - return this.message.type === TestMessageType.Error ? this.message.contextValue : undefined; - } - - constructor(public readonly result: ITestResult, test: TestResultItem, public readonly taskIndex: number, public readonly messageIndex: number) { - this.test = test.item; - const messages = test.tasks[taskIndex].messages; - this.messageIndex = messageIndex; - - const parts = { messageIndex, resultId: result.id, taskIndex, testExtId: test.item.extId }; - this.expectedUri = buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }); - this.actualUri = buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }); - this.messageUri = buildTestUri({ ...parts, type: TestUriType.ResultMessage }); - - const message = this.message = messages[this.messageIndex]; - this.context = getMessageArgs(test, message); - this.revealLocation = message.location ?? (test.item.uri && test.item.range ? { uri: test.item.uri, range: Range.lift(test.item.range) } : undefined); - } -} - -class TaskSubject { - public readonly outputUri: URI; - public readonly revealLocation: undefined; - - constructor(public readonly result: ITestResult, public readonly taskIndex: number) { - this.outputUri = buildTestUri({ resultId: result.id, taskIndex, type: TestUriType.TaskOutput }); - } -} - -class TestOutputSubject { - public readonly outputUri: URI; - public readonly revealLocation: undefined; - public readonly task: ITestRunTask; - - constructor(public readonly result: ITestResult, public readonly taskIndex: number, public readonly test: TestResultItem) { - this.outputUri = buildTestUri({ resultId: this.result.id, taskIndex: this.taskIndex, testExtId: this.test.item.extId, type: TestUriType.TestOutput }); - this.task = result.tasks[this.taskIndex]; - } -} - -type InspectSubject = MessageSubject | TaskSubject | TestOutputSubject; - -const equalsSubject = (a: InspectSubject, b: InspectSubject) => ( - (a instanceof MessageSubject && b instanceof MessageSubject && a.message === b.message) || - (a instanceof TaskSubject && b instanceof TaskSubject && a.result === b.result && a.taskIndex === b.taskIndex) || - (a instanceof TestOutputSubject && b instanceof TestOutputSubject && a.test === b.test && a.taskIndex === b.taskIndex) -); /** Iterates through every message in every result */ function* allMessages(results: readonly ITestResult[]) { @@ -203,6 +97,13 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener target: StorageTarget.USER, }, this.storageService)), false); + /** @inheritdoc */ + public readonly callStackVisible = MutableObservableValue.stored(this._register(new StoredValue({ + key: 'testCallStackVisible', + scope: StorageScope.PROFILE, + target: StorageTarget.USER, + }, this.storageService)), true); + constructor( @IConfigurationService private readonly configuration: IConfigurationService, @IEditorService private readonly editorService: IEditorService, @@ -213,9 +114,16 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener @IViewsService private readonly viewsService: IViewsService, @ICommandService private readonly commandService: ICommandService, @INotificationService private readonly notificationService: INotificationService, + @IContextKeyService contextKeyService: IContextKeyService, ) { super(); this._register(testResults.onTestChanged(this.openPeekOnFailure, this)); + + const callStackVisibleKey = TestingContextKeys.showCallStackInPeek.bindTo(contextKeyService); + this._register(this.callStackVisible.onDidChange(() => { + callStackVisibleKey.set(this.callStackVisible.value); + })); + callStackVisibleKey.set(this.callStackVisible.value); } /** @inheritdoc */ @@ -501,20 +409,6 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener } } -const mapFindTestMessage = (test: TestResultItem, fn: (task: ITestTaskState, message: ITestMessage, messageIndex: number, taskIndex: number) => T | undefined) => { - for (let taskIndex = 0; taskIndex < test.tasks.length; taskIndex++) { - const task = test.tasks[taskIndex]; - for (let messageIndex = 0; messageIndex < task.messages.length; messageIndex++) { - const r = fn(task, task.messages[messageIndex], messageIndex, taskIndex); - if (r !== undefined) { - return r; - } - } - } - - return undefined; -}; - /** * Adds output/message peek functionality to code editors. */ @@ -531,11 +425,6 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo */ private readonly peek = this._register(new MutableDisposable()); - /** - * URI of the currently-visible peek, if any. - */ - private currentPeekUri: URI | undefined; - /** * Context key updated when the peek is visible/hidden. */ @@ -562,31 +451,24 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo this._register(testResults.onTestChanged(this.closePeekOnTestChange, this)); } - /** - * Toggles peek visibility for the URI. - */ - public toggle(uri: URI) { - if (this.currentPeekUri?.toString() === uri.toString()) { - this.peek.clear(); - } else { - this.show(uri); - } - } - /** * Shows a peek for the message in the editor. */ public async show(uri: URI) { const subject = this.retrieveTest(uri); - if (!subject) { - return; + if (subject) { + this.showSubject(subject); } + } + /** + * Shows a peek for the existing inspect subject. + */ + public async showSubject(subject: InspectSubject, frame?: ITestMessageStackFrame, uiState?: ITestResultsViewContentUiState) { if (!this.peek.value) { this.peek.value = this.instantiationService.createInstance(TestResultsPeek, this.editor); this.peek.value.onDidClose(() => { this.visible.set(false); - this.currentPeekUri = undefined; this.peek.value = undefined; }); @@ -598,8 +480,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo alert(renderTestMessageAsText(subject.message.message)); } - this.peek.value.setModel(subject); - this.currentPeekUri = uri; + this.peek.value.setModel(subject, frame, uiState); } public async openAndShow(uri: URI) { @@ -772,274 +653,6 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo } } -const FOLLOWUP_ANIMATION_MIN_TIME = 500; - -class FollowupActionWidget extends Disposable { - private readonly el = dom.h('div.testing-followup-action', []); - private readonly visibleStore = this._register(new DisposableStore()); - - constructor( - private readonly container: HTMLElement, - @ITestService private readonly testService: ITestService, - @IQuickInputService private readonly quickInput: IQuickInputService, - ) { - super(); - } - - public show(subject: InspectSubject) { - this.visibleStore.clear(); - if (subject instanceof MessageSubject) { - this.showMessage(subject); - } - } - - private async showMessage(subject: MessageSubject) { - const cts = this.visibleStore.add(new CancellationTokenSource()); - const start = Date.now(); - - // Wait for completion otherwise results will not be available to the ext host: - if (subject.result instanceof LiveTestResult && !subject.result.completedAt) { - await new Promise(r => Event.once((subject.result as LiveTestResult).onComplete)(r)); - } - - const followups = await this.testService.provideTestFollowups({ - extId: subject.test.extId, - messageIndex: subject.messageIndex, - resultId: subject.result.id, - taskIndex: subject.taskIndex, - }, cts.token); - - - if (!followups.followups.length || cts.token.isCancellationRequested) { - followups.dispose(); - return; - } - - this.visibleStore.add(followups); - - dom.clearNode(this.el.root); - this.el.root.classList.toggle('animated', Date.now() - start > FOLLOWUP_ANIMATION_MIN_TIME); - - this.el.root.appendChild(this.makeFollowupLink(followups.followups[0])); - if (followups.followups.length > 1) { - this.el.root.appendChild(this.makeMoreLink(followups.followups)); - } - - this.container.appendChild(this.el.root); - this.visibleStore.add(toDisposable(() => { - this.el.root.parentElement?.removeChild(this.el.root); - })); - } - - private makeFollowupLink(first: ITestFollowup) { - const link = this.makeLink(() => this.actionFollowup(link, first)); - dom.reset(link, ...renderLabelWithIcons(first.message)); - return link; - } - - private makeMoreLink(followups: ITestFollowup[]) { - const link = this.makeLink(() => - this.quickInput.pick(followups.map((f, i) => ({ - label: f.message, - index: i - }))).then(picked => { - if (picked?.length) { - followups[picked[0].index].execute(); - } - }) - ); - - link.innerText = localize('testFollowup.more', '+{0} More...', followups.length - 1); - return link; - } - - private makeLink(onClick: () => void) { - const link = document.createElement('a'); - link.tabIndex = 0; - this.visibleStore.add(dom.addDisposableListener(link, 'click', onClick)); - this.visibleStore.add(dom.addDisposableListener(link, 'keydown', e => { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { - onClick(); - } - })); - - return link; - } - - private actionFollowup(link: HTMLAnchorElement, fu: ITestFollowup) { - if (link.ariaDisabled !== 'true') { - link.ariaDisabled = 'true'; - fu.execute(); - } - } -} - -class TestResultsViewContent extends Disposable { - private static lastSplitWidth?: number; - - private readonly didReveal = this._register(new Emitter<{ subject: InspectSubject; preserveFocus: boolean }>()); - private readonly currentSubjectStore = this._register(new DisposableStore()); - private followupWidget!: FollowupActionWidget; - private messageContextKeyService!: IContextKeyService; - private contextKeyTestMessage!: IContextKey; - private contextKeyResultOutdated!: IContextKey; - - private dimension?: dom.Dimension; - private splitView!: SplitView; - private messageContainer!: HTMLElement; - private contentProviders!: IPeekOutputRenderer[]; - private contentProvidersUpdateLimiter = this._register(new Limiter(1)); - - public current?: InspectSubject; - - /** Fired when a tree item is selected. Populated only on .fillBody() */ - public onDidRequestReveal!: Event; - - constructor( - private readonly editor: ICodeEditor | undefined, - private readonly options: { - historyVisible: IObservableValue; - showRevealLocationOnMessages: boolean; - locationForProgress: string; - }, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextModelService protected readonly modelService: ITextModelService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - ) { - super(); - } - - public fillBody(containerElement: HTMLElement): void { - const initialSpitWidth = TestResultsViewContent.lastSplitWidth; - this.splitView = new SplitView(containerElement, { orientation: Orientation.HORIZONTAL }); - - 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.contentProviders = [ - this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)), - this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)), - this._register(this.instantiationService.createInstance(TerminalMessagePeek, messageContainer, isInPeekView)), - this._register(this.instantiationService.createInstance(PlainTextMessagePeek, this.editor, messageContainer)), - ]; - - this.messageContextKeyService = this._register(this.contextKeyService.createScoped(containerElement)); - this.contextKeyTestMessage = TestingContextKeys.testMessageContext.bindTo(this.messageContextKeyService); - this.contextKeyResultOutdated = TestingContextKeys.testResultOutdated.bindTo(this.messageContextKeyService); - - const treeContainer = dom.append(containerElement, dom.$('.test-output-peek-tree')); - const tree = this._register(this.instantiationService.createInstance( - OutputPeekTree, - treeContainer, - this.didReveal.event, - { showRevealLocationOnMessages, locationForProgress: this.options.locationForProgress }, - )); - - this.onDidRequestReveal = tree.onDidRequestReview; - - this.splitView.addView({ - onDidChange: Event.None, - element: messageContainer, - minimumSize: 200, - maximumSize: Number.MAX_VALUE, - layout: width => { - TestResultsViewContent.lastSplitWidth = width; - if (this.dimension) { - for (const provider of this.contentProviders) { - provider.layout({ height: this.dimension.height, width }); - } - } - }, - }, Sizing.Distribute); - - this.splitView.addView({ - onDidChange: Event.None, - element: treeContainer, - minimumSize: 100, - maximumSize: Number.MAX_VALUE, - layout: width => { - if (this.dimension) { - tree.layout(this.dimension.height, width); - } - }, - }, Sizing.Distribute); - - const historyViewIndex = 1; - this.splitView.setViewVisible(historyViewIndex, historyVisible.value); - this._register(historyVisible.onDidChange(visible => { - this.splitView.setViewVisible(historyViewIndex, visible); - })); - - if (initialSpitWidth) { - queueMicrotask(() => this.splitView.resizeView(0, initialSpitWidth)); - } - } - - /** - * Shows a message in-place without showing or changing the peek location. - * This is mostly used if peeking a message without a location. - */ - public reveal(opts: { subject: InspectSubject; preserveFocus: boolean }) { - this.didReveal.fire(opts); - - if (this.current && equalsSubject(this.current, opts.subject)) { - return Promise.resolve(); - } - - this.current = opts.subject; - return this.contentProvidersUpdateLimiter.queue(async () => { - await Promise.all(this.contentProviders.map(p => p.update(opts.subject))); - this.followupWidget.show(opts.subject); - this.currentSubjectStore.clear(); - this.populateFloatingClick(opts.subject); - }); - } - - private populateFloatingClick(subject: InspectSubject) { - if (!(subject instanceof MessageSubject)) { - return; - } - - this.currentSubjectStore.add(toDisposable(() => { - this.contextKeyResultOutdated.reset(); - this.contextKeyTestMessage.reset(); - })); - - this.contextKeyTestMessage.set(subject.contextValue || ''); - if (subject.result instanceof LiveTestResult) { - this.contextKeyResultOutdated.set(subject.result.getStateById(subject.test.extId)?.retired ?? false); - this.currentSubjectStore.add(subject.result.onChange(ev => { - if (ev.item.item.extId === subject.test.extId) { - this.contextKeyResultOutdated.set(ev.item.retired ?? false); - } - })); - } else { - this.contextKeyResultOutdated.set(true); - } - - - this.currentSubjectStore.add( - this.instantiationService - .createChild(new ServiceCollection([IContextKeyService, this.messageContextKeyService])) - .createInstance(FloatingClickMenu, { - container: this.messageContainer, - menuId: MenuId.TestMessageContent, - getActionArg: () => (subject as MessageSubject).context, - }) - ); - } - - public onLayoutBody(height: number, width: number) { - this.dimension = new dom.Dimension(width, height); - this.splitView.layout(width); - } - - public onWidth(width: number) { - this.splitView.layout(width); - } -} class TestResultsPeek extends PeekViewWidget { private static lastHeightInLines?: number; @@ -1059,6 +672,8 @@ class TestResultsPeek extends PeekViewWidget { @IMenuService private readonly menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, @ITextModelService protected readonly modelService: ITextModelService, + @ICodeEditorService protected readonly codeEditorService: ICodeEditorService, + @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, ) { super(editor, { showFrame: true, frameWidth: 1, showArrow: true, isResizeable: true, isAccessible: true, className: 'test-output-peek' }, instantiationService); @@ -1086,22 +701,84 @@ 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 })); + + this._disposables.add(this.content.onClose(() => { + TestingOutputPeekController.get(this.editor)?.removePeek(); + })); + + this._disposables.add(this.content.onDidChangeStackFrame(sf => { + if (!sf.uri) { + return; + } + + if (this.uriIdentityService.extUri.isEqual(sf.uri, this.editor.getModel()?.uri)) { + if (sf.position) { + this.changePositionTo(sf.position); + } + return; + } + + const current = this.current; + const uiState = this.content.uiState; + this.codeEditorService.openCodeEditor( + { + resource: sf.uri, + options: { + preserveFocus: true, + selection: sf.position ? Range.fromPositions(sf.position) : undefined, + transient: true, + } + }, + this.editor, + ).then(newEditor => { + if (!newEditor || !current) { + return; + } + + TestingOutputPeekController.get(newEditor)?.showSubject(current, sf, uiState); + }); + })); } super._fillContainer(container); } + /** Moves the peek to a new position in the current editor, maintaining its position on the screen */ + private changePositionTo(position: IPosition) { + const currentPosition = this.position; + if (!currentPosition) { + this.show(position, TestResultsPeek.lastHeightInLines || 10); + return; + } + + const currentPositionViewOffset = this.editor.getScrolledVisiblePosition(currentPosition); + this.updatePositionAndHeight(position); + + if (currentPositionViewOffset) { + const newPosition = this.editor.getTopForPosition(position.lineNumber, position.column); + this.editor.setScrollTop(newPosition - currentPositionViewOffset?.top); + } + } protected override _fillHead(container: HTMLElement): void { super._fillHead(container); - const actions: IAction[] = []; const menu = this.menuService.createMenu(MenuId.TestPeekTitle, this.contextKeyService); + const actionBar = this._actionbarWidget!; + this._disposables.add(menu.onDidChange(() => { + actions.length = 0; + createAndFillInActionBarActions(menu, undefined, actions); + while (actionBar.getAction(1)) { + actionBar.pull(0); // remove all but the view's default "close" button + } + actionBar.push(actions, { label: false, icon: true, index: 0 }); + })); + + const actions: IAction[] = []; createAndFillInActionBarActions(menu, undefined, actions); - this._actionbarWidget!.push(actions, { label: false, icon: true, index: 0 }); - menu.dispose(); + actionBar.push(actions, { label: false, icon: true, index: 0 }); } protected override _fillBody(containerElement: HTMLElement): void { @@ -1116,35 +793,35 @@ class TestResultsPeek extends PeekViewWidget { /** * Updates the test to be shown. */ - public setModel(subject: InspectSubject): Promise { + public setModel(subject: InspectSubject, frame?: ITestMessageStackFrame, uiState?: ITestResultsViewContentUiState): Promise { if (subject instanceof TaskSubject || subject instanceof TestOutputSubject) { this.current = subject; - return this.showInPlace(subject); + return this.showInPlace(subject, frame, uiState); } const message = subject.message; const previous = this.current; - if (!subject.revealLocation && !previous) { + const revealLocation = frame?.position || subject.revealLocation?.range.getStartPosition(); + if (!revealLocation && !previous) { return Promise.resolve(); } this.current = subject; - if (!subject.revealLocation) { - return this.showInPlace(subject); + if (!revealLocation) { + return this.showInPlace(subject, frame, uiState); } - this.show(subject.revealLocation.range, TestResultsPeek.lastHeightInLines || hintMessagePeekHeight(message)); - const startPosition = subject.revealLocation.range.getStartPosition(); - this.editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(startPosition), ScrollType.Smooth); + this.show(revealLocation, TestResultsPeek.lastHeightInLines || hintMessagePeekHeight(message)); + this.editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(revealLocation), ScrollType.Smooth); - return this.showInPlace(subject); + return this.showInPlace(subject, frame, uiState); } /** * Shows a message in-place without showing or changing the peek location. * This is mostly used if peeking a message without a location. */ - public async showInPlace(subject: InspectSubject) { + public async showInPlace(subject: InspectSubject, frame?: ITestMessageStackFrame, uiState?: ITestResultsViewContentUiState) { if (subject instanceof MessageSubject) { const message = subject.message; this.setTitle(firstLine(renderTestMessageAsText(message.message)), stripIcons(subject.test.label)); @@ -1152,7 +829,7 @@ class TestResultsPeek extends PeekViewWidget { this.setTitle(localize('testOutputTitle', 'Test Output')); } this.applyTheme(); - await this.content.reveal({ subject: subject, preserveFocus: false }); + await this.content.reveal({ subject, frame, preserveFocus: false, uiState }); } protected override _relayout(newHeightInLines: number): void { @@ -1197,6 +874,7 @@ export class TestResultsView extends ViewPane { @ITelemetryService telemetryService: ITelemetryService, @IHoverService hoverService: IHoverService, @ITestResultService private readonly resultService: ITestResultService, + @IEditorService private readonly editorService: IEditorService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); } @@ -1233,7 +911,18 @@ export class TestResultsView extends ViewPane { private renderContent(container: HTMLElement) { const content = this.content.value; content.fillBody(container); - content.onDidRequestReveal(subject => content.reveal({ preserveFocus: true, subject })); + this._register(content.onDidRequestReveal(subject => content.reveal({ preserveFocus: true, subject }))); + this._register(content.onDidChangeStackFrame(sf => { + if (sf.uri) { + this.editorService.openEditor({ + resource: sf.uri, + options: { + preserveFocus: true, + selection: sf.position ? Range.fromPositions(sf.position) : undefined, + } + }); + } + })); const [lastResult] = this.resultService.results; if (lastResult && lastResult.tasks.length) { @@ -1242,1354 +931,86 @@ export class TestResultsView extends ViewPane { } } -interface IPeekOutputRenderer extends IDisposable { - /** Updates the displayed test. Should clear if it cannot display the test. */ - update(subject: InspectSubject): void; - /** Recalculate content layout. */ - layout(dimension: dom.IDimension): void; - /** Dispose the content provider. */ - dispose(): void; -} +const hintMessagePeekHeight = (msg: ITestMessage) => { + const msgHeight = ITestMessage.isDiffable(msg) + ? Math.max(hintPeekStrHeight(msg.actual), hintPeekStrHeight(msg.expected)) + : hintPeekStrHeight(typeof msg.message === 'string' ? msg.message : msg.message.value); -const commonEditorOptions: IEditorOptions = { - scrollBeyondLastLine: false, - links: true, - lineNumbers: 'off', - scrollbar: { - verticalScrollbarSize: 14, - horizontal: 'auto', - useShadows: true, - verticalHasArrows: false, - horizontalHasArrows: false, - alwaysConsumeMouseWheel: false - }, - fixedOverflowWidgets: true, - readOnly: true, - minimap: { - enabled: false - }, - wordWrap: 'on', + // add 8ish lines for the size of the title and decorations in the peek. + return msgHeight + 8; }; -const diffEditorOptions: IDiffEditorConstructionOptions = { - ...commonEditorOptions, - enableSplitViewResizing: true, - isInEmbeddedEditor: true, - renderOverviewRuler: false, - ignoreTrimWhitespace: false, - renderSideBySide: true, - useInlineViewWhenSpaceIsLimited: false, - originalAriaLabel: localize('testingOutputExpected', 'Expected result'), - modifiedAriaLabel: localize('testingOutputActual', 'Actual result'), - diffAlgorithm: 'advanced', +const firstLine = (str: string) => { + const index = str.indexOf('\n'); + return index === -1 ? str : str.slice(0, index); }; -const isDiffable = (message: ITestMessage): message is ITestErrorMessage & { actual: string; expected: string } => - message.type === TestMessageType.Error && message.actual !== undefined && message.expected !== undefined; - -class DiffContentProvider extends Disposable implements IPeekOutputRenderer { - private readonly widget = this._register(new MutableDisposable()); - private readonly model = this._register(new MutableDisposable()); - private dimension?: dom.IDimension; - constructor( - private readonly editor: ICodeEditor | undefined, - private readonly container: HTMLElement, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextModelService private readonly modelService: ITextModelService, - ) { - super(); - } +const hintPeekStrHeight = (str: string) => Math.min(count(str, '\n'), 24); - public async update(subject: InspectSubject) { - if (!(subject instanceof MessageSubject)) { - return this.clear(); - } - const message = subject.message; - if (!isDiffable(message)) { - return this.clear(); - } +function getOuterEditorFromDiffEditor(codeEditorService: ICodeEditorService): ICodeEditor | null { + const diffEditors = codeEditorService.listDiffEditors(); - const [original, modified] = await Promise.all([ - this.modelService.createModelReference(subject.expectedUri), - this.modelService.createModelReference(subject.actualUri), - ]); - - const model = this.model.value = new SimpleDiffEditorModel(original, modified); - if (!this.widget.value) { - this.widget.value = this.editor ? this.instantiationService.createInstance( - EmbeddedDiffEditorWidget, - this.container, - diffEditorOptions, - {}, - this.editor, - ) : this.instantiationService.createInstance( - DiffEditorWidget, - this.container, - diffEditorOptions, - {}, - ); - - if (this.dimension) { - this.widget.value.layout(this.dimension); - } + for (const diffEditor of diffEditors) { + if (diffEditor.hasTextFocus() && diffEditor instanceof EmbeddedDiffEditorWidget) { + return diffEditor.getParentEditor(); } - - this.widget.value.setModel(model); - this.widget.value.updateOptions(this.getOptions( - isMultiline(message.expected) || isMultiline(message.actual) - )); } - private clear() { - this.model.clear(); - this.widget.clear(); - } + return null; +} - public layout(dimensions: dom.IDimension) { - this.dimension = dimensions; - this.widget.value?.layout(dimensions); +export class CloseTestPeek extends EditorAction2 { + constructor() { + super({ + id: 'editor.closeTestPeek', + title: localize2('close', 'Close'), + icon: Codicon.close, + precondition: ContextKeyExpr.or(TestingContextKeys.isInPeek, TestingContextKeys.isPeekVisible), + keybinding: { + weight: KeybindingWeight.EditorContrib - 101, + primary: KeyCode.Escape, + when: ContextKeyExpr.not('config.editor.stablePeek') + } + }); } - protected getOptions(isMultiline: boolean): IDiffEditorOptions { - return isMultiline - ? { ...diffEditorOptions, lineNumbers: 'on' } - : { ...diffEditorOptions, lineNumbers: 'off' }; + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void { + const parent = getPeekedEditorFromFocus(accessor.get(ICodeEditorService)); + TestingOutputPeekController.get(parent ?? editor)?.removePeek(); } } -class ScrollableMarkdownMessage extends Disposable { - private readonly scrollable: DomScrollableElement; - private readonly element: HTMLElement; - - constructor(container: HTMLElement, markdown: MarkdownRenderer, message: IMarkdownString) { - super(); - - const rendered = this._register(markdown.render(message, {})); - rendered.element.style.height = '100%'; - rendered.element.style.userSelect = 'text'; - container.appendChild(rendered.element); - this.element = rendered.element; - this.scrollable = this._register(new DomScrollableElement(rendered.element, { - className: 'preview-text', - })); - container.appendChild(this.scrollable.getDomNode()); +const navWhen = ContextKeyExpr.and( + EditorContextKeys.focus, + TestingContextKeys.isPeekVisible, +); - this._register(toDisposable(() => { - container.removeChild(this.scrollable.getDomNode()); - })); +/** + * Gets the appropriate editor for peeking based on the currently focused editor. + */ +const getPeekedEditorFromFocus = (codeEditorService: ICodeEditorService) => { + const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); + return editor && getPeekedEditor(codeEditorService, editor); +}; - this.scrollable.scanDomNode(); +/** + * Gets the editor where the peek may be shown, bubbling upwards if the given + * editor is embedded (i.e. inside a peek already). + */ +const getPeekedEditor = (codeEditorService: ICodeEditorService, editor: ICodeEditor) => { + if (TestingOutputPeekController.get(editor)?.subject) { + return editor; } - public layout(height: number, width: number) { - // Remove padding of `.monaco-editor .zone-widget.test-output-peek .preview-text` - this.scrollable.setScrollDimensions({ - width: width - 32, - height: height - 16, - scrollWidth: this.element.scrollWidth, - scrollHeight: this.element.scrollHeight - }); + if (editor instanceof EmbeddedCodeEditorWidget) { + return editor.getParentEditor(); } -} - -class MarkdownTestMessagePeek extends Disposable implements IPeekOutputRenderer { - private readonly markdown = new Lazy( - () => this._register(this.instantiationService.createInstance(MarkdownRenderer, {})), - ); - - private readonly textPreview = this._register(new MutableDisposable()); - constructor(private readonly container: HTMLElement, @IInstantiationService private readonly instantiationService: IInstantiationService) { - super(); - } - - public update(subject: InspectSubject): void { - if (!(subject instanceof MessageSubject)) { - return this.textPreview.clear(); - } - - const message = subject.message; - if (isDiffable(message) || typeof message.message === 'string') { - return this.textPreview.clear(); - } - - this.textPreview.value = new ScrollableMarkdownMessage( - this.container, - this.markdown.value, - message.message as IMarkdownString, - ); - } - - public layout(dimension: dom.IDimension): void { - this.textPreview.value?.layout(dimension.height, dimension.width); - } -} - -class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer { - private readonly widgetDecorations = this._register(new MutableDisposable()); - private readonly widget = this._register(new MutableDisposable()); - private readonly model = this._register(new MutableDisposable()); - private dimension?: dom.IDimension; - - constructor( - private readonly editor: ICodeEditor | undefined, - private readonly container: HTMLElement, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextModelService private readonly modelService: ITextModelService, - ) { - super(); - } - - public async update(subject: InspectSubject) { - if (!(subject instanceof MessageSubject)) { - return this.clear(); - } - - const message = subject.message; - if (isDiffable(message) || message.type === TestMessageType.Output || typeof message.message !== 'string') { - return this.clear(); - } - - const modelRef = this.model.value = await this.modelService.createModelReference(subject.messageUri); - if (!this.widget.value) { - this.widget.value = this.editor ? this.instantiationService.createInstance( - EmbeddedCodeEditorWidget, - this.container, - commonEditorOptions, - {}, - this.editor, - ) : this.instantiationService.createInstance( - CodeEditorWidget, - this.container, - commonEditorOptions, - { isSimpleWidget: true } - ); - - if (this.dimension) { - this.widget.value.layout(this.dimension); - } - } - - this.widget.value.setModel(modelRef.object.textEditorModel); - this.widget.value.updateOptions(commonEditorOptions); - this.widgetDecorations.value = colorizeTestMessageInEditor(message.message, this.widget.value); - } - - private clear() { - this.widgetDecorations.clear(); - this.widget.clear(); - this.model.clear(); - } - - public layout(dimensions: dom.IDimension) { - this.dimension = dimensions; - this.widget.value?.layout(dimensions); - } -} - -class TerminalMessagePeek extends Disposable implements IPeekOutputRenderer { - private dimensions?: dom.IDimension; - private readonly terminalCwd = this._register(new MutableObservableValue('')); - private readonly xtermLayoutDelayer = this._register(new Delayer(50)); - - /** Active terminal instance. */ - private readonly terminal = this._register(new MutableDisposable()); - /** Listener for streaming result data */ - private readonly outputDataListener = this._register(new MutableDisposable()); - - constructor( - private readonly container: HTMLElement, - private readonly isInPeekView: boolean, - @ITerminalService private readonly terminalService: ITerminalService, - @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, - @IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService, - ) { - super(); - } - - private async makeTerminal() { - const prev = this.terminal.value; - if (prev) { - prev.xterm.clearBuffer(); - prev.xterm.clearSearchDecorations(); - // clearBuffer tries to retain the prompt line, but this doesn't exist for tests. - // So clear the screen (J) and move to home (H) to ensure previous data is cleaned up. - prev.xterm.write(`\x1b[2J\x1b[0;0H`); - return prev; - } - - const capabilities = new TerminalCapabilityStore(); - const cwd = this.terminalCwd; - capabilities.add(TerminalCapability.CwdDetection, { - type: TerminalCapability.CwdDetection, - get cwds() { return [cwd.value]; }, - onDidChangeCwd: cwd.onDidChange, - getCwd: () => cwd.value, - updateCwd: () => { }, - }); - - return this.terminal.value = await this.terminalService.createDetachedTerminal({ - rows: 10, - cols: 80, - readonly: true, - capabilities, - processInfo: new DetachedProcessInfo({ initialCwd: cwd.value }), - colorProvider: { - getBackgroundColor: theme => { - const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); - if (terminalBackground) { - return terminalBackground; - } - if (this.isInPeekView) { - return theme.getColor(peekViewResultsBackground); - } - const location = this.viewDescriptorService.getViewLocationById(Testing.ResultsViewId); - return location === ViewContainerLocation.Panel - ? theme.getColor(PANEL_BACKGROUND) - : theme.getColor(SIDE_BAR_BACKGROUND); - }, - } - }); - } - - public async update(subject: InspectSubject) { - this.outputDataListener.clear(); - if (subject instanceof TaskSubject) { - await this.updateForTaskSubject(subject); - } else if (subject instanceof TestOutputSubject || (subject instanceof MessageSubject && subject.message.type === TestMessageType.Output)) { - await this.updateForTestSubject(subject); - } else { - this.clear(); - } - } - - private async updateForTestSubject(subject: TestOutputSubject | MessageSubject) { - const that = this; - const testItem = subject instanceof TestOutputSubject ? subject.test.item : subject.test; - const terminal = await this.updateGenerically({ - subject, - noOutputMessage: localize('caseNoOutput', 'The test case did not report any output.'), - getTarget: result => result?.tasks[subject.taskIndex].output, - *doInitialWrite(output, results) { - that.updateCwd(testItem.uri); - const state = subject instanceof TestOutputSubject ? subject.test : results.getStateById(testItem.extId); - if (!state) { - return; - } - - for (const message of state.tasks[subject.taskIndex].messages) { - if (message.type === TestMessageType.Output) { - yield* output.getRangeIter(message.offset, message.length); - } - } - }, - doListenForMoreData: (output, result, write) => result.onChange(e => { - if (e.reason === TestResultItemChangeReason.NewMessage && e.item.item.extId === testItem.extId && e.message.type === TestMessageType.Output) { - for (const chunk of output.getRangeIter(e.message.offset, e.message.length)) { - write(chunk.buffer); - } - } - }), - }); - - if (subject instanceof MessageSubject && subject.message.type === TestMessageType.Output && subject.message.marker !== undefined) { - terminal?.xterm.selectMarkedRange(getMarkId(subject.message.marker, true), getMarkId(subject.message.marker, false), /* scrollIntoView= */ true); - } - } - - private updateForTaskSubject(subject: TaskSubject) { - return this.updateGenerically({ - subject, - noOutputMessage: localize('runNoOutput', 'The test run did not record any output.'), - getTarget: result => result?.tasks[subject.taskIndex], - doInitialWrite: (task, result) => { - // Update the cwd and use the first test to try to hint at the correct cwd, - // but often this will fall back to the first workspace folder. - this.updateCwd(Iterable.find(result.tests, t => !!t.item.uri)?.item.uri); - return task.output.buffers; - }, - doListenForMoreData: (task, _result, write) => task.output.onDidWriteData(e => write(e.buffer)), - }); - } - - private async updateGenerically(opts: { - subject: InspectSubject; - noOutputMessage: string; - getTarget: (result: ITestResult) => T | undefined; - doInitialWrite: (target: T, result: LiveTestResult) => Iterable; - doListenForMoreData: (target: T, result: LiveTestResult, write: (s: Uint8Array) => void) => IDisposable; - }) { - const result = opts.subject.result; - const target = opts.getTarget(result); - if (!target) { - return this.clear(); - } - - const terminal = await this.makeTerminal(); - let didWriteData = false; - - const pendingWrites = new MutableObservableValue(0); - if (result instanceof LiveTestResult) { - for (const chunk of opts.doInitialWrite(target, result)) { - didWriteData ||= chunk.byteLength > 0; - pendingWrites.value++; - terminal.xterm.write(chunk.buffer, () => pendingWrites.value--); - } - } else { - didWriteData = true; - this.writeNotice(terminal, localize('runNoOutputForPast', 'Test output is only available for new test runs.')); - } - - this.attachTerminalToDom(terminal); - this.outputDataListener.clear(); - - if (result instanceof LiveTestResult && !result.completedAt) { - const l1 = result.onComplete(() => { - if (!didWriteData) { - this.writeNotice(terminal, opts.noOutputMessage); - } - }); - const l2 = opts.doListenForMoreData(target, result, data => { - terminal.xterm.write(data); - didWriteData ||= data.byteLength > 0; - }); - - this.outputDataListener.value = combinedDisposable(l1, l2); - } - - if (!this.outputDataListener.value && !didWriteData) { - this.writeNotice(terminal, opts.noOutputMessage); - } - - // Ensure pending writes finish, otherwise the selection in `updateForTestSubject` - // can happen before the markers are processed. - if (pendingWrites.value > 0) { - await new Promise(resolve => { - const l = pendingWrites.onDidChange(() => { - if (pendingWrites.value === 0) { - l.dispose(); - resolve(); - } - }); - }); - } - - return terminal; - } - - private updateCwd(testUri?: URI) { - const wf = (testUri && this.workspaceContext.getWorkspaceFolder(testUri)) - || this.workspaceContext.getWorkspace().folders[0]; - if (wf) { - this.terminalCwd.value = wf.uri.fsPath; - } - } - - private writeNotice(terminal: IDetachedTerminalInstance, str: string) { - terminal.xterm.write(formatMessageForTerminal(str)); - } - - private attachTerminalToDom(terminal: IDetachedTerminalInstance) { - terminal.xterm.write('\x1b[?25l'); // hide cursor - dom.scheduleAtNextAnimationFrame(dom.getWindow(this.container), () => this.layoutTerminal(terminal)); - terminal.attachToElement(this.container, { enableGpu: false }); - } - - private clear() { - this.outputDataListener.clear(); - this.xtermLayoutDelayer.cancel(); - this.terminal.clear(); - } - - public layout(dimensions: dom.IDimension) { - this.dimensions = dimensions; - if (this.terminal.value) { - this.layoutTerminal(this.terminal.value, dimensions.width, dimensions.height); - } - } - - private layoutTerminal( - { xterm }: IDetachedTerminalInstance, - width = this.dimensions?.width ?? this.container.clientWidth, - height = this.dimensions?.height ?? this.container.clientHeight - ) { - width -= 10 + 20; // scrollbar width + margin - this.xtermLayoutDelayer.trigger(() => { - const scaled = getXtermScaledDimensions(dom.getWindow(this.container), xterm.getFont(), width, height); - if (scaled) { - xterm.resize(scaled.cols, scaled.rows); - } - }); - } -} - -const hintMessagePeekHeight = (msg: ITestMessage) => { - const msgHeight = isDiffable(msg) - ? Math.max(hintPeekStrHeight(msg.actual), hintPeekStrHeight(msg.expected)) - : hintPeekStrHeight(typeof msg.message === 'string' ? msg.message : msg.message.value); - - // add 8ish lines for the size of the title and decorations in the peek. - return msgHeight + 8; -}; - -const firstLine = (str: string) => { - const index = str.indexOf('\n'); - return index === -1 ? str : str.slice(0, index); -}; - -const isMultiline = (str: string | undefined) => !!str && str.includes('\n'); - -const hintPeekStrHeight = (str: string) => Math.min(count(str, '\n'), 24); - -class SimpleDiffEditorModel extends EditorModel { - public readonly original = this._original.object.textEditorModel; - public readonly modified = this._modified.object.textEditorModel; - - constructor( - private readonly _original: IReference, - private readonly _modified: IReference, - ) { - super(); - } - - public override dispose() { - super.dispose(); - this._original.dispose(); - this._modified.dispose(); - } -} - -function getOuterEditorFromDiffEditor(codeEditorService: ICodeEditorService): ICodeEditor | null { - const diffEditors = codeEditorService.listDiffEditors(); - - for (const diffEditor of diffEditors) { - if (diffEditor.hasTextFocus() && diffEditor instanceof EmbeddedDiffEditorWidget) { - return diffEditor.getParentEditor(); - } - } - - return null; -} - -export class CloseTestPeek extends EditorAction2 { - constructor() { - super({ - id: 'editor.closeTestPeek', - title: localize2('close', 'Close'), - icon: Codicon.close, - precondition: ContextKeyExpr.or(TestingContextKeys.isInPeek, TestingContextKeys.isPeekVisible), - keybinding: { - weight: KeybindingWeight.EditorContrib - 101, - primary: KeyCode.Escape, - when: ContextKeyExpr.not('config.editor.stablePeek') - } - }); - } - - runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void { - const parent = getPeekedEditorFromFocus(accessor.get(ICodeEditorService)); - TestingOutputPeekController.get(parent ?? editor)?.removePeek(); - } -} - -interface ITreeElement { - type: string; - context: unknown; - id: string; - label: string; - onDidChange: Event; - labelWithIcons?: readonly (HTMLSpanElement | string)[]; - icon?: ThemeIcon; - description?: string; - ariaLabel?: string; -} - -class TestResultElement implements ITreeElement { - public readonly changeEmitter = new Emitter(); - public readonly onDidChange = this.changeEmitter.event; - public readonly type = 'result'; - public readonly context = this.value.id; - public readonly id = this.value.id; - public readonly label = this.value.name; - - public get icon() { - return icons.testingStatesToIcons.get( - this.value.completedAt === undefined - ? TestResultState.Running - : maxCountPriority(this.value.counts) - ); - } - - constructor(public readonly value: ITestResult) { } -} - -const openCoverageLabel = localize('openTestCoverage', 'View Test Coverage'); -const closeCoverageLabel = localize('closeTestCoverage', 'Close Test Coverage'); - -class CoverageElement implements ITreeElement { - public readonly type = 'coverage'; - public readonly context: undefined; - public readonly id = `coverage-${this.results.id}/${this.task.id}`; - public readonly onDidChange: Event; - - public get label() { - return this.isOpen ? closeCoverageLabel : openCoverageLabel; - } - - public get icon() { - return this.isOpen ? widgetClose : icons.testingCoverageReport; - } - - public get isOpen() { - return this.coverageService.selected.get()?.fromTaskId === this.task.id; - } - - constructor( - private readonly results: ITestResult, - public readonly task: ITestRunTaskResults, - private readonly coverageService: ITestCoverageService, - ) { - this.onDidChange = Event.fromObservableLight(coverageService.selected); - } - -} - -class TestCaseElement implements ITreeElement { - public readonly type = 'test'; - public readonly context: ITestItemContext = { - $mid: MarshalledId.TestItemContext, - tests: [InternalTestItem.serialize(this.test)], - }; - public readonly id = `${this.results.id}/${this.test.item.extId}`; - public readonly description?: string; - - public get onDidChange() { - if (!(this.results instanceof LiveTestResult)) { - return Event.None; - } - - return Event.filter(this.results.onChange, e => e.item.item.extId === this.test.item.extId); - } - - public get state() { - return this.test.tasks[this.taskIndex].state; - } - - public get label() { - return this.test.item.label; - } - - public get labelWithIcons() { - return renderLabelWithIcons(this.label); - } - - public get icon() { - return icons.testingStatesToIcons.get(this.state); - } - - public get outputSubject() { - return new TestOutputSubject(this.results, this.taskIndex, this.test); - } - - - constructor( - public readonly results: ITestResult, - public readonly test: TestResultItem, - public readonly taskIndex: number, - ) { } -} - -class TaskElement implements ITreeElement { - public readonly changeEmitter = new Emitter(); - public readonly onDidChange = this.changeEmitter.event; - public readonly type = 'task'; - public readonly context: string; - public readonly id: string; - public readonly label: string; - public readonly itemsCache = new CreationCache(); - - public get icon() { - return this.results.tasks[this.index].running ? icons.testingStatesToIcons.get(TestResultState.Running) : undefined; - } - - constructor(public readonly results: ITestResult, public readonly task: ITestRunTaskResults, public readonly index: number) { - this.id = `${results.id}/${index}`; - this.task = results.tasks[index]; - this.context = String(index); - this.label = this.task.name ?? localize('testUnnamedTask', 'Unnamed Task'); - } -} - -class TestMessageElement implements ITreeElement { - public readonly type = 'message'; - public readonly id: string; - public readonly label: string; - public readonly uri: URI; - public readonly location?: IRichLocation; - public readonly description?: string; - public readonly contextValue?: string; - public readonly message: ITestMessage; - - public get onDidChange() { - if (!(this.result instanceof LiveTestResult)) { - return Event.None; - } - - // rerender when the test case changes so it gets retired events - return Event.filter(this.result.onChange, e => e.item.item.extId === this.test.item.extId); - } - - public get context(): ITestMessageMenuArgs { - return getMessageArgs(this.test, this.message); - } - - public get outputSubject() { - return new TestOutputSubject(this.result, this.taskIndex, this.test); - } - - constructor( - public readonly result: ITestResult, - public readonly test: TestResultItem, - public readonly taskIndex: number, - public readonly messageIndex: number, - ) { - const m = this.message = test.tasks[taskIndex].messages[messageIndex]; - - this.location = m.location; - this.contextValue = m.type === TestMessageType.Error ? m.contextValue : undefined; - this.uri = buildTestUri({ - type: TestUriType.ResultMessage, - messageIndex, - resultId: result.id, - taskIndex, - testExtId: test.item.extId - }); - - this.id = this.uri.toString(); - - const asPlaintext = renderTestMessageAsText(m.message); - const lines = count(asPlaintext.trimEnd(), '\n'); - this.label = firstLine(asPlaintext); - if (lines > 0) { - this.description = lines > 1 - ? localize('messageMoreLinesN', '+ {0} more lines', lines) - : localize('messageMoreLines1', '+ 1 more line'); - } - } -} - -type TreeElement = TestResultElement | TestCaseElement | TestMessageElement | TaskElement | CoverageElement; - -class OutputPeekTree extends Disposable { - private disposed = false; - private readonly tree: WorkbenchCompressibleObjectTree; - private readonly treeActions: TreeActionsProvider; - private readonly requestReveal = this._register(new Emitter()); - - public readonly onDidRequestReview = this.requestReveal.event; - - constructor( - container: HTMLElement, - onDidReveal: Event<{ subject: InspectSubject; preserveFocus: boolean }>, - options: { showRevealLocationOnMessages: boolean; locationForProgress: string }, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @ITestResultService results: ITestResultService, - @IInstantiationService instantiationService: IInstantiationService, - @ITestExplorerFilterState explorerFilter: ITestExplorerFilterState, - @ITestCoverageService coverageService: ITestCoverageService, - @IProgressService progressService: IProgressService, - ) { - super(); - - this.treeActions = instantiationService.createInstance(TreeActionsProvider, options.showRevealLocationOnMessages, this.requestReveal,); - const diffIdentityProvider: IIdentityProvider = { - getId(e: TreeElement) { - return e.id; - } - }; - - this.tree = this._register(instantiationService.createInstance( - WorkbenchCompressibleObjectTree, - 'Test Output Peek', - container, - { - getHeight: () => 22, - getTemplateId: () => TestRunElementRenderer.ID, - }, - [instantiationService.createInstance(TestRunElementRenderer, this.treeActions)], - { - compressionEnabled: true, - hideTwistiesOfChildlessElements: true, - identityProvider: diffIdentityProvider, - sorter: { - compare(a, b) { - if (a instanceof TestCaseElement && b instanceof TestCaseElement) { - return cmpPriority(a.state, b.state); - } - - return 0; - }, - }, - accessibilityProvider: { - getAriaLabel(element: ITreeElement) { - return element.ariaLabel || element.label; - }, - getWidgetAriaLabel() { - return localize('testingPeekLabel', 'Test Result Messages'); - } - } - }, - )) as WorkbenchCompressibleObjectTree; - - const cc = new CreationCache(); - const getTaskChildren = (taskElem: TaskElement): Iterable> => { - const { results, index, itemsCache, task } = taskElem; - const tests = Iterable.filter(results.tests, test => test.tasks[index].state >= TestResultState.Running || test.tasks[index].messages.length > 0); - let result: Iterable> = Iterable.map(tests, test => ({ - element: itemsCache.getOrCreate(test, () => new TestCaseElement(results, test, index)), - incompressible: true, - children: getTestChildren(results, test, index), - })); - - if (task.coverage.get()) { - result = Iterable.concat( - Iterable.single>({ - element: new CoverageElement(results, task, coverageService), - incompressible: true, - }), - result, - ); - } - - return result; - }; - - const getTestChildren = (result: ITestResult, test: TestResultItem, taskIndex: number): Iterable> => { - return test.tasks[taskIndex].messages - .map((m, messageIndex) => - m.type === TestMessageType.Error - ? { element: cc.getOrCreate(m, () => new TestMessageElement(result, test, taskIndex, messageIndex)), incompressible: false } - : undefined - ) - .filter(isDefined); - }; - - const getResultChildren = (result: ITestResult): Iterable> => { - return result.tasks.map((task, taskIndex) => { - const taskElem = cc.getOrCreate(task, () => new TaskElement(result, task, taskIndex)); - return ({ - element: taskElem, - incompressible: false, - children: getTaskChildren(taskElem), - }); - }); - }; - - const getRootChildren = () => results.results.map(result => { - const element = cc.getOrCreate(result, () => new TestResultElement(result)); - return { - element, - incompressible: true, - collapsed: this.tree.hasElement(element) ? this.tree.isCollapsed(element) : true, - children: getResultChildren(result) - }; - }); - - // Queued result updates to prevent spamming CPU when lots of tests are - // completing and messaging quickly (#142514) - const taskChildrenToUpdate = new Set(); - const taskChildrenUpdate = this._register(new RunOnceScheduler(() => { - for (const taskNode of taskChildrenToUpdate) { - if (this.tree.hasElement(taskNode)) { - this.tree.setChildren(taskNode, getTaskChildren(taskNode), { diffIdentityProvider }); - } - } - taskChildrenToUpdate.clear(); - }, 300)); - - const queueTaskChildrenUpdate = (taskNode: TaskElement) => { - taskChildrenToUpdate.add(taskNode); - if (!taskChildrenUpdate.isScheduled()) { - taskChildrenUpdate.schedule(); - } - }; - - const attachToResults = (result: LiveTestResult) => { - const resultNode = cc.get(result)! as TestResultElement; - const disposable = new DisposableStore(); - disposable.add(result.onNewTask(i => { - if (result.tasks.length === 1) { - this.requestReveal.fire(new TaskSubject(result, 0)); // reveal the first task in new runs - } - - if (this.tree.hasElement(resultNode)) { - this.tree.setChildren(resultNode, getResultChildren(result), { diffIdentityProvider }); - } - - // note: tasks are bounded and their lifetime is equivalent to that of - // the test result, so this doesn't leak indefinitely. - const task = result.tasks[i]; - disposable.add(autorun(reader => { - task.coverage.read(reader); // add it to the autorun - queueTaskChildrenUpdate(cc.get(task) as TaskElement); - })); - })); - disposable.add(result.onEndTask(index => { - (cc.get(result.tasks[index]) as TaskElement | undefined)?.changeEmitter.fire(); - })); - - disposable.add(result.onChange(e => { - // try updating the item in each of its tasks - for (const [index, task] of result.tasks.entries()) { - const taskNode = cc.get(task) as TaskElement; - if (!this.tree.hasElement(taskNode)) { - continue; - } - - const itemNode = taskNode.itemsCache.get(e.item); - if (itemNode && this.tree.hasElement(itemNode)) { - if (e.reason === TestResultItemChangeReason.NewMessage && e.message.type === TestMessageType.Error) { - this.tree.setChildren(itemNode, getTestChildren(result, e.item, index), { diffIdentityProvider }); - } - return; - } - - queueTaskChildrenUpdate(taskNode); - } - })); - - disposable.add(result.onComplete(() => { - resultNode.changeEmitter.fire(); - disposable.dispose(); - })); - - return resultNode; - }; - - this._register(results.onResultsChanged(e => { - // little hack here: a result change can cause the peek to be disposed, - // but this listener will still be queued. Doing stuff with the tree - // will cause errors. - if (this.disposed) { - return; - } - - if ('completed' in e) { - (cc.get(e.completed) as TestResultElement | undefined)?.changeEmitter.fire(); - return; - } - - this.tree.setChildren(null, getRootChildren(), { diffIdentityProvider }); - - // done after setChildren intentionally so that the ResultElement exists in the cache. - if ('started' in e) { - for (const child of this.tree.getNode(null).children) { - this.tree.collapse(child.element, false); - } - - this.tree.expand(attachToResults(e.started), true); - } - })); - - const revealItem = (element: TreeElement, preserveFocus: boolean) => { - this.tree.setFocus([element]); - this.tree.setSelection([element]); - if (!preserveFocus) { - this.tree.domFocus(); - } - }; - - this._register(onDidReveal(async ({ subject, preserveFocus = false }) => { - if (subject instanceof TaskSubject) { - const resultItem = this.tree.getNode(null).children.find(c => { - if (c.element instanceof TaskElement) { - return c.element.results.id === subject.result.id && c.element.index === subject.taskIndex; - } - if (c.element instanceof TestResultElement) { - return c.element.id === subject.result.id; - } - return false; - }); - - if (resultItem) { - revealItem(resultItem.element!, preserveFocus); - } - return; - } - - const revealElement = subject instanceof TestOutputSubject - ? cc.get(subject.task)?.itemsCache.get(subject.test) - : cc.get(subject.message); - if (!revealElement || !this.tree.hasElement(revealElement)) { - return; - } - - const parents: TreeElement[] = []; - for (let parent = this.tree.getParentElement(revealElement); parent; parent = this.tree.getParentElement(parent)) { - parents.unshift(parent); - } - - for (const parent of parents) { - this.tree.expand(parent); - } - - if (this.tree.getRelativeTop(revealElement) === null) { - this.tree.reveal(revealElement, 0.5); - } - - revealItem(revealElement, preserveFocus); - })); - - this._register(this.tree.onDidOpen(async e => { - if (e.element instanceof TestMessageElement) { - this.requestReveal.fire(new MessageSubject(e.element.result, e.element.test, e.element.taskIndex, e.element.messageIndex)); - } else if (e.element instanceof TestCaseElement) { - const t = e.element; - const message = mapFindTestMessage(e.element.test, (_t, _m, mesasgeIndex, taskIndex) => - new MessageSubject(t.results, t.test, taskIndex, mesasgeIndex)); - this.requestReveal.fire(message || new TestOutputSubject(t.results, 0, t.test)); - } else if (e.element instanceof CoverageElement) { - const task = e.element.task; - if (e.element.isOpen) { - return coverageService.closeCoverage(); - } - progressService.withProgress( - { location: options.locationForProgress }, - () => coverageService.openCoverage(task, true) - ); - } - })); - - this._register(this.tree.onDidChangeSelection(evt => { - for (const element of evt.elements) { - if (element && 'test' in element) { - explorerFilter.reveal.value = element.test.item.extId; - break; - } - } - })); - - - this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); - - this.tree.setChildren(null, getRootChildren()); - for (const result of results.results) { - if (!result.completedAt && result instanceof LiveTestResult) { - attachToResults(result); - } - } - } - - public layout(height: number, width: number) { - this.tree.layout(height, width); - } - - private onContextMenu(evt: ITreeContextMenuEvent) { - if (!evt.element) { - return; - } - - const actions = this.treeActions.provideActionBar(evt.element); - this.contextMenuService.showContextMenu({ - getAnchor: () => evt.anchor, - getActions: () => actions.secondary.length - ? [...actions.primary, new Separator(), ...actions.secondary] - : actions.primary, - getActionsContext: () => evt.element?.context - }); - } - - public override dispose() { - super.dispose(); - this.disposed = true; - } -} - -interface TemplateData { - label: HTMLElement; - icon: HTMLElement; - actionBar: ActionBar; - elementDisposable: DisposableStore; - templateDisposable: DisposableStore; -} - -class TestRunElementRenderer implements ICompressibleTreeRenderer { - public static readonly ID = 'testRunElementRenderer'; - public readonly templateId = TestRunElementRenderer.ID; - - constructor( - private readonly treeActions: TreeActionsProvider, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { } - - /** @inheritdoc */ - public renderCompressedElements(node: ITreeNode, FuzzyScore>, _index: number, templateData: TemplateData): void { - const chain = node.element.elements; - const lastElement = chain[chain.length - 1]; - if ((lastElement instanceof TaskElement || lastElement instanceof TestMessageElement) && chain.length >= 2) { - this.doRender(chain[chain.length - 2], templateData, lastElement); - } else { - this.doRender(lastElement, templateData); - } - } - - /** @inheritdoc */ - public renderTemplate(container: HTMLElement): TemplateData { - const templateDisposable = new DisposableStore(); - const wrapper = dom.append(container, dom.$('.test-peek-item')); - const icon = dom.append(wrapper, dom.$('.state')); - const label = dom.append(wrapper, dom.$('.name')); - - const actionBar = new ActionBar(wrapper, { - actionViewItemProvider: (action, options) => - action instanceof MenuItemAction - ? this.instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate }) - : undefined - }); - - const elementDisposable = new DisposableStore(); - templateDisposable.add(elementDisposable); - templateDisposable.add(actionBar); - - return { - icon, - label, - actionBar, - elementDisposable, - templateDisposable, - }; - } - - /** @inheritdoc */ - public renderElement(element: ITreeNode, _index: number, templateData: TemplateData): void { - this.doRender(element.element, templateData); - } - - /** @inheritdoc */ - public disposeTemplate(templateData: TemplateData): void { - templateData.templateDisposable.dispose(); - } - - /** Called to render a new element */ - private doRender(element: ITreeElement, templateData: TemplateData, subjectElement?: ITreeElement) { - templateData.elementDisposable.clear(); - templateData.elementDisposable.add( - element.onDidChange(() => this.doRender(element, templateData, subjectElement)), - ); - this.doRenderInner(element, templateData, subjectElement); - } - - /** Called, and may be re-called, to render or re-render an element */ - private doRenderInner(element: ITreeElement, templateData: TemplateData, subjectElement: ITreeElement | undefined) { - let { label, labelWithIcons, description } = element; - if (subjectElement instanceof TestMessageElement) { - description = subjectElement.label; - } - - const descriptionElement = description ? dom.$('span.test-label-description', {}, description) : ''; - if (labelWithIcons) { - dom.reset(templateData.label, ...labelWithIcons, descriptionElement); - } else { - dom.reset(templateData.label, label, descriptionElement); - } - - const icon = element.icon; - templateData.icon.className = `computed-state ${icon ? ThemeIcon.asClassName(icon) : ''}`; - - const actions = this.treeActions.provideActionBar(element); - templateData.actionBar.clear(); - templateData.actionBar.context = element.context; - templateData.actionBar.push(actions.primary, { icon: true, label: false }); - } -} - -class TreeActionsProvider { - constructor( - private readonly showRevealLocationOnMessages: boolean, - private readonly requestReveal: Emitter, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - @ICommandService private readonly commandService: ICommandService, - @ITestProfileService private readonly testProfileService: ITestProfileService, - @IEditorService private readonly editorService: IEditorService, - ) { } - - public provideActionBar(element: ITreeElement) { - const test = element instanceof TestCaseElement ? element.test : undefined; - const capabilities = test ? this.testProfileService.capabilitiesForTest(test) : 0; - - const contextKeys: [string, unknown][] = [ - ['peek', Testing.OutputPeekContributionId], - [TestingContextKeys.peekItemType.key, element.type], - ]; - - let id = MenuId.TestPeekElement; - const primary: IAction[] = []; - const secondary: IAction[] = []; - - if (element instanceof TaskElement) { - primary.push(new Action( - 'testing.outputPeek.showResultOutput', - localize('testing.showResultOutput', "Show Result Output"), - ThemeIcon.asClassName(Codicon.terminal), - undefined, - () => this.requestReveal.fire(new TaskSubject(element.results, element.index)), - )); - } - - if (element instanceof TestResultElement) { - // only show if there are no collapsed test nodes that have more specific choices - if (element.value.tasks.length === 1) { - primary.push(new Action( - 'testing.outputPeek.showResultOutput', - localize('testing.showResultOutput', "Show Result Output"), - ThemeIcon.asClassName(Codicon.terminal), - undefined, - () => this.requestReveal.fire(new TaskSubject(element.value, 0)), - )); - } - - primary.push(new Action( - 'testing.outputPeek.reRunLastRun', - localize('testing.reRunLastRun', "Rerun Test Run"), - ThemeIcon.asClassName(icons.testingRunIcon), - undefined, - () => this.commandService.executeCommand('testing.reRunLastRun', element.value.id), - )); - - if (capabilities & TestRunProfileBitset.Debug) { - primary.push(new Action( - 'testing.outputPeek.debugLastRun', - localize('testing.debugLastRun', "Debug Test Run"), - ThemeIcon.asClassName(icons.testingDebugIcon), - undefined, - () => this.commandService.executeCommand('testing.debugLastRun', element.value.id), - )); - } - } - - if (element instanceof TestCaseElement || element instanceof TestMessageElement) { - contextKeys.push( - [TestingContextKeys.testResultOutdated.key, element.test.retired], - [TestingContextKeys.testResultState.key, testResultStateToContextValues[element.test.ownComputedState]], - ...getTestItemContextOverlay(element.test, capabilities), - ); - - const extId = element.test.item.extId; - if (element.test.tasks[element.taskIndex].messages.some(m => m.type === TestMessageType.Output)) { - primary.push(new Action( - 'testing.outputPeek.showResultOutput', - localize('testing.showResultOutput', "Show Result Output"), - ThemeIcon.asClassName(Codicon.terminal), - undefined, - () => this.requestReveal.fire(element.outputSubject), - )); - } - - secondary.push(new Action( - 'testing.outputPeek.revealInExplorer', - localize('testing.revealInExplorer', "Reveal in Test Explorer"), - ThemeIcon.asClassName(Codicon.listTree), - undefined, - () => this.commandService.executeCommand('_revealTestInExplorer', extId), - )); - - if (capabilities & TestRunProfileBitset.Run) { - primary.push(new Action( - 'testing.outputPeek.runTest', - localize('run test', 'Run Test'), - ThemeIcon.asClassName(icons.testingRunIcon), - undefined, - () => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Run, extId), - )); - } - - if (capabilities & TestRunProfileBitset.Debug) { - primary.push(new Action( - 'testing.outputPeek.debugTest', - localize('debug test', 'Debug Test'), - ThemeIcon.asClassName(icons.testingDebugIcon), - undefined, - () => this.commandService.executeCommand('vscode.runTestsById', TestRunProfileBitset.Debug, extId), - )); - } - - } - - if (element instanceof TestMessageElement) { - primary.push(new Action( - 'testing.outputPeek.goToFile', - localize('testing.goToFile', "Go to Source"), - ThemeIcon.asClassName(Codicon.goToFile), - undefined, - () => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId), - )); - } - - if (element instanceof TestMessageElement) { - id = MenuId.TestMessageContext; - contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]); - if (this.showRevealLocationOnMessages && element.location) { - primary.push(new Action( - 'testing.outputPeek.goToError', - localize('testing.goToError', "Go to Source"), - ThemeIcon.asClassName(Codicon.goToFile), - undefined, - () => this.editorService.openEditor({ - resource: element.location!.uri, - options: { - selection: element.location!.range, - preserveFocus: true, - } - }), - )); - } - } - - - 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 navWhen = ContextKeyExpr.and( - EditorContextKeys.focus, - TestingContextKeys.isPeekVisible, -); - -/** - * Gets the appropriate editor for peeking based on the currently focused editor. - */ -const getPeekedEditorFromFocus = (codeEditorService: ICodeEditorService) => { - const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); - return editor && getPeekedEditor(codeEditorService, editor); -}; - -/** - * Gets the editor where the peek may be shown, bubbling upwards if the given - * editor is embedded (i.e. inside a peek already). - */ -const getPeekedEditor = (codeEditorService: ICodeEditorService, editor: ICodeEditor) => { - if (TestingOutputPeekController.get(editor)?.subject) { - return editor; - } - - if (editor instanceof EmbeddedCodeEditorWidget) { - return editor.getParentEditor(); - } - - const outer = getOuterEditorFromDiffEditor(codeEditorService); - if (outer) { - return outer; + const outer = getOuterEditorFromDiffEditor(codeEditorService); + if (outer) { + return outer; } return editor; @@ -2716,21 +1137,34 @@ export class ToggleTestingPeekHistory extends Action2 { } } -class CreationCache { - private readonly v = new WeakMap(); - - public get(key: object): T2 | undefined { - return this.v.get(key) as T2 | undefined; +export class ToggleCallStackAction extends Action2 { + public static readonly ID = 'testing.toggleCallStack'; + constructor() { + super({ + id: ToggleCallStackAction.ID, + title: localize2('testing.toggleCallStack', 'Show Call Stack in Peek'), + metadata: { + description: localize2('testing.toggleCallStack.description', 'Shows or hides the history of test runs in the peek view') + }, + icon: Codicon.debugLineByLine, + toggled: TestingContextKeys.showCallStackInPeek, + category: Categories.Test, + menu: [{ + id: MenuId.TestCallStackContext, + }, { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', Testing.ResultsViewId) + }, { + id: MenuId.TestPeekTitle, + group: 'navigation', + when: ContextKeyExpr.not(TestingContextKeys.showCallStackInPeek.key), + order: 4, + }], + }); } - public getOrCreate(ref: object, factory: () => T2): T2 { - const existing = this.v.get(ref); - if (existing) { - return existing as T2; - } - - const fresh = factory(); - this.v.set(ref, fresh); - return fresh; + public override run(accessor: ServicesAccessor) { + const opener = accessor.get(ITestingPeekOpener); + opener.callStackVisible.value = !opener.callStackVisible.value; } } diff --git a/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index 536c03da5f99f..0c088a787c36e 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 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 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 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 testingColorRunAction = registerColor('testing.runAction', testingColorIconPassed, localize('testing.runAction', "Color for 'run' icons in the editor.")); + +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', '#848484', localize('testing.iconUnset', "Color for the 'Unset' 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 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 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 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 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 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 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', 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', 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', 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', 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', 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/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index 321434bd6029a..aae8f0af3622e 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; + constructor(coverage: IFileCoverage, fromResult: LiveTestResult, private readonly accessor: ICoverageAccessor) { + super(coverage, fromResult); + } /** - * If this is for a single test item, gets the test item. + * Gets per-line coverage details. */ - public isForTest?: { id: TestId; parent: FileCoverage }; + 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; + } - constructor(coverage: IFileCoverage, fromResult: LiveTestResult, private readonly accessor: ICoverageAccessor) { - super(coverage, fromResult); + 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 9dfbb798a612a..4433a713d2caa 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'; @@ -89,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 3ab130d114fae..e9c1275a79c06 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 79fcec77b1dbc..0b66669342a54 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 adf73855e6cb8..d01101c6919dd 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 3035be654dffc..88bdd37b5c294 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 d3026db22eb8a..48ebe0e230e05 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 fd3ffcd0999fb..7b0e47cd891bf 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 75f7b37136209..1a99445285fa1 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[]; @@ -169,6 +169,32 @@ export const enum TestMessageType { Output } +export interface ITestMessageStackFrame { + label: string; + uri: URI | undefined; + position: Position | undefined; +} + +export namespace ITestMessageStackFrame { + export interface Serialized { + label: string; + uri: UriComponents | undefined; + position: IPosition | undefined; + } + + export const serialize = (stack: Readonly): Serialized => ({ + label: stack.label, + uri: stack.uri?.toJSON(), + position: stack.position?.toJSON(), + }); + + export const deserialize = (uriIdentity: ITestUriCanonicalizer, stack: Serialized): ITestMessageStackFrame => ({ + label: stack.label, + uri: stack.uri ? uriIdentity.asCanonicalUri(URI.revive(stack.uri)) : undefined, + position: stack.position ? Position.lift(stack.position) : undefined, + }); +} + export interface ITestErrorMessage { message: string | IMarkdownString; type: TestMessageType.Error; @@ -176,6 +202,7 @@ export interface ITestErrorMessage { actual: string | undefined; contextValue: string | undefined; location: IRichLocation | undefined; + stackTrace: undefined | ITestMessageStackFrame[]; } export namespace ITestErrorMessage { @@ -186,6 +213,7 @@ export namespace ITestErrorMessage { actual: string | undefined; contextValue: string | undefined; location: IRichLocation.Serialize | undefined; + stackTrace: undefined | ITestMessageStackFrame.Serialized[]; } export const serialize = (message: Readonly): Serialized => ({ @@ -195,6 +223,7 @@ export namespace ITestErrorMessage { actual: message.actual, contextValue: message.contextValue, location: message.location && IRichLocation.serialize(message.location), + stackTrace: message.stackTrace?.map(ITestMessageStackFrame.serialize), }); export const deserialize = (uriIdentity: ITestUriCanonicalizer, message: Serialized): ITestErrorMessage => ({ @@ -204,6 +233,7 @@ export namespace ITestErrorMessage { actual: message.actual, contextValue: message.contextValue, location: message.location && IRichLocation.deserialize(uriIdentity, message.location), + stackTrace: message.stackTrace && message.stackTrace.map(s => ITestMessageStackFrame.deserialize(uriIdentity, s)), }); } @@ -258,6 +288,9 @@ export namespace ITestMessage { export const deserialize = (uriIdentity: ITestUriCanonicalizer, message: Serialized): ITestMessage => message.type === TestMessageType.Error ? ITestErrorMessage.deserialize(uriIdentity, message) : ITestOutputMessage.deserialize(uriIdentity, message); + + export const isDiffable = (message: ITestMessage): message is ITestErrorMessage & { actual: string; expected: string } => + message.type === TestMessageType.Error && message.actual !== undefined && message.expected !== undefined; } export interface ITestTaskState { @@ -573,7 +606,7 @@ export namespace ICoverageCount { export interface IFileCoverage { id: string; uri: URI; - testId?: TestId; + testIds?: string[]; statement: ICoverageCount; branch?: ICoverageCount; declaration?: ICoverageCount; @@ -583,7 +616,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 +627,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 +636,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 1463fb3c1d482..648b6b06797c1 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -26,6 +26,7 @@ export namespace TestingContextKeys { 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 showCallStackInPeek = new RawContextKey('testing.showCallStackInPeek', true, { type: 'boolean', description: localize('testing.showCallStackInPeek', 'Whether to show the failure call stack in the testing peek') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, @@ -75,4 +76,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 b8053615c6b0c..47be6dfacba61 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/common/testingPeekOpener.ts b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts index c00dbf9ad51bc..ec5d980e1eb3a 100644 --- a/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts +++ b/src/vs/workbench/contrib/testing/common/testingPeekOpener.ts @@ -24,6 +24,9 @@ export interface ITestingPeekOpener { /** Whether test history should be shown in the results output. */ historyVisible: MutableObservableValue; + /** Whether the test call stack should be shown in the results output. */ + callStackVisible: MutableObservableValue; + /** * Tries to peek the first test error, if the item is in a failed state. * @returns a boolean indicating whether a peek was opened 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 ea3ab5dfe0f8f..f0709ccc4e574 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 cebf9e01a5dac..d59d15ef1f878 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 ea8bb1e5b6e9a..9ab4c5536497d 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 2be30fb7d2aa3..192515892c716 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 1d5771ae4bb36..c83d3e7d3b9a1 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 da47d907d00f8..4ccd1f6da369c 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 d040484a930d1..c7c61884c44fe 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 0000000000000..66ae8c791f3c4 --- /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 836fe2143e8d7..c32350bc356f0 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 6dd030f9b530d..6e9ce5b745ae9 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 ee279e630894c..656f9ec5cabc0 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 f152f086e9dd1..41c6e84efe7af 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 9f6cc07ba5f90..78f429a64bb6d 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 fa3edab7d3acc..9e34bee7036cd 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 baab39f82b690..db1c149501f97 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 0233dfd53099b..86a5a8d011982 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 .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-list { + margin-top: 15px; } -.profiles-editor .sidebar-container .profiles-tree .profile-tree-item { +.profiles-editor .sidebar-container .profiles-list .profile-list-item { + padding-left: 20px; display: flex; align-items: center; } -.profiles-editor .sidebar-container .profiles-tree .profile-tree-item > * { +.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-actions-container { +.profiles-editor .contents-container .profile-header .profile-button-container { display: flex; - height: 28px; + align-items: center; } -.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container { - gap: 4px; +.profiles-editor .contents-container .profile-header .profile-button-container .monaco-button { + margin-left: 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 { + display: flex; } .profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container { - margin-right: 5px; + margin-right: 6px; min-width: 120px; } @@ -96,26 +152,12 @@ padding-right: 10px; } -.profiles-editor .contents-container .profile-body { - margin-top: 20px; +.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container .action-label { + padding: 6px; } -.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-body { + margin-top: 20px; } .profiles-editor .contents-container .profile-select-container { @@ -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: grid; + align-items: center; +} + +.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-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-tree-item-container.new-profile-resource-type-container > .profile-resource-type-label-container { - width: 150px; +.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 { +.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 f215f1fcbfd60..29d961380898d 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -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'; @@ -51,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(); @@ -70,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(); @@ -80,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( @@ -99,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())); @@ -116,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(); @@ -123,6 +163,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements submenu: ProfilesMenu, group: '2_configuration', order: 1, + when, }); MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { get title() { @@ -131,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)); + } } } @@ -226,6 +235,42 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements }); } + 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() { @@ -240,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(); + } }); } @@ -266,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); + } } }); } @@ -304,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() } ] }); @@ -334,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 } @@ -343,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, { @@ -372,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, @@ -493,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 } ] @@ -501,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(); + } } })); } @@ -519,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 a9b85a44e65ba..c19f31930f815 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 4c294d03ac234..f10759523705d 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,29 +202,58 @@ 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 { @@ -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 bde549ecefaaf..237d9142b36c2 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,51 +189,142 @@ 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; + save(): void { + this.saveScheduler.schedule(); + } + + 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(); + if (this.message) { + return; + } + const useDefaultFlags: UseDefaultProfileFlags | undefined = this.flags + ? this.flags.settings && this.flags.keybindings && this.flags.tasks && this.flags.globalState && this.flags.extensions ? undefined : this.flags + : undefined; + + return await this.userDataProfileManagementService.updateProfile(profile, { + name: this.name, + icon: this.icon, + useDefaultFlags: profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags + }); + } + + abstract readonly titleButtons: [Action[], Action[]]; abstract readonly titleActions: [IAction[], IAction[]]; - abstract readonly contextMenuActions: IAction[]; + abstract readonly actions: [IAction[], IAction[]]; + + protected abstract doSave(): Promise; } -export class UserDataProfileElement extends AbstractUserDataProfileElement implements IProfileElement { +export class UserDataProfileElement extends AbstractUserDataProfileElement { get profile(): IUserDataProfile { return this._profile; } - readonly primaryAction = undefined; - - private readonly saveScheduler = this._register(new RunOnceScheduler(() => this.doSave(), 500)); - constructor( private _profile: IUserDataProfile, + readonly titleButtons: [Action[], Action[]], readonly titleActions: [IAction[], IAction[]], - readonly contextMenuActions: IAction[], + readonly actions: [IAction[], IAction[]], @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUserDataProfileManagementService userDataProfileManagementService: IUserDataProfileManagementService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @ICommandService commandService: ICommandService, @IInstantiationService instantiationService: IInstantiationService, ) { super( @@ -203,9 +332,18 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple _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); @@ -216,52 +354,34 @@ export class UserDataProfileElement extends AbstractUserDataProfileElement imple 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; + 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); } - return false; - } - - save(): void { - this.saveScheduler.schedule(); } - private async doSave(): Promise { - if (!this.hasUnsavedChanges()) { - return; - } - this.validate(); - if (this.message) { - return; + 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 }); } - const useDefaultFlags: UseDefaultProfileFlags | undefined = this.flags - ? 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, { - name: this.name, - icon: this.icon, - useDefaultFlags: this.profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags - }); + protected override async doSave(): Promise { + await this.saveProfile(this.profile); } - override async getChildren(resourceType: ProfileResourceType): Promise { + 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 titlePrimaryActions: IAction[] = []; - titlePrimaryActions.push(activateAction); - titlePrimaryActions.push(exportAction); - if (!profile.isDefault) { - titlePrimaryActions.push(deleteAction); - } + 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(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); - })); + 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, - localize('untitled', "Untitled"), + copyFrom ? '' : localize('untitled', "Untitled"), copyFrom, - disposables.add(new Action('userDataProfile.create', localize('create', "Create & Apply"), undefined, true, () => this.saveNewProfile())), - [[discardAction], []], - [discardAction], + [[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; } + if (this.newProfileElement.previewProfile) { + return; + } + const profile = await this.saveNewProfile(true, token); + if (profile) { + this.newProfileElement.previewProfile = profile; + await this.openWindow(profile); + } + } + + async saveNewProfile(transient?: boolean, token?: CancellationToken): Promise { + if (!this.newProfileElement) { + return undefined; + } + this.newProfileElement.validate(); if (this.newProfileElement.message) { - return; + return undefined; } - 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) { - 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 }); + 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; + } } - this.removeNewProfile(); - const profile = this.userDataProfilesService.profiles.find(p => p.name === name); - if (profile) { + 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/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 5e094fc4ccc40..ae571f8c84d78 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 fa7b15e39c854..f46e124042850 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 a6e9943b8669d..e5fa674ea8224 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 eda7179665f24..75ee4b7306135 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 3d5d21f080f7f..8870ddf4cc639 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 eae9c80fa6872..dde553ec05724 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/webviewWindowDragMonitor.ts b/src/vs/workbench/contrib/webview/browser/webviewWindowDragMonitor.ts index e4dc5eaf0e904..d009ae186da59 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 f24731711d3ca..3777d363bbf5e 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/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 1d374e644c3c0..664c55b7172f9 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 8e934b891a3f6..e3227a80af487 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 869f44526b48d..fef4b4928bf46 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 50144d87e1c07..caab78fd561c4 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 b7b09ca9417ca..3a63341f1d8b3 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 ed362ff32c5d4..7ab127eaab4a7 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 c2c1e099961e4..34efe4d154eb7 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts @@ -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 0fbe0079ed1ae..4ae3a9f94edb0 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -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', @@ -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 73ebdb1b2a959..140e12ee47d75 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 6791e00042cc0..17d4e15c4facf 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; @@ -169,6 +169,12 @@ const apiMenus: IAPIMenu[] = [ description: localize('menus.input', "The Source Control input box menu"), proposed: 'contribSourceControlInputBoxMenu' }, + { + key: 'scm/historyItemChanges/title', + id: MenuId.SCMChangesSeparator, + description: localize('menus.historyItemChanges', "The Source Control incoming/outgoing changes title menu"), + proposed: 'contribSourceControlHistoryItemChangesMenu' + }, { key: 'scm/incomingChanges', id: MenuId.SCMIncomingChanges, @@ -343,6 +349,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 52e00d57f7b40..db9b4febfbd6d 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 84cc7730f4bf8..d154337b9a5c1 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -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.`); diff --git a/src/vs/workbench/services/authentication/common/authentication.ts b/src/vs/workbench/services/authentication/common/authentication.ts index 6da6e530237c6..1d99ffc059ad2 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 854aea75cdac2..891c718ac73a4 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/clipboard/browser/clipboardService.ts b/src/vs/workbench/services/clipboard/browser/clipboardService.ts index fc2b1b582e680..1c1472d0cc1f2 100644 --- a/src/vs/workbench/services/clipboard/browser/clipboardService.ts +++ b/src/vs/workbench/services/clipboard/browser/clipboardService.ts @@ -28,7 +28,19 @@ export class BrowserClipboardService extends BaseBrowserClipboardService { super(layoutService, logService); } + override async writeText(text: string, type?: string): Promise { + if (!!this.environmentService.extensionTestsLocationURI && typeof type !== 'string') { + type = 'vscode-tests'; // force in-memory clipboard for tests to avoid permission issues + } + + return super.writeText(text, type); + } + override async readText(type?: string): Promise { + if (!!this.environmentService.extensionTestsLocationURI && typeof type !== 'string') { + type = 'vscode-tests'; // force in-memory clipboard for tests to avoid permission issues + } + if (type) { return super.readText(type); } @@ -36,10 +48,6 @@ export class BrowserClipboardService extends BaseBrowserClipboardService { try { return await getActiveWindow().navigator.clipboard.readText(); } catch (error) { - if (!!this.environmentService.extensionTestsLocationURI) { - return ''; // do not ask for input in tests (https://github.com/microsoft/vscode/issues/112264) - } - return new Promise(resolve => { // Inform user about permissions problem (https://github.com/microsoft/vscode/issues/112089) diff --git a/src/vs/workbench/services/commands/test/common/commandService.test.ts b/src/vs/workbench/services/commands/test/common/commandService.test.ts index 38c6f24d3f456..0c87ce176e1b9 100644 --- a/src/vs/workbench/services/commands/test/common/commandService.test.ts +++ b/src/vs/workbench/services/commands/test/common/commandService.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, DisposableStore } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { CommandService } from 'vs/workbench/services/commands/common/commandService'; diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index b44f1e122353e..c1bac6f869193 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -1260,9 +1260,9 @@ class RegisterConfigurationSchemasContribution extends Disposable implements IWo type: 'object', description: localize('configurationDefaults.description', 'Contribute defaults for configurations'), properties: Object.assign({}, - machineOverridableSettings.properties, - windowSettings.properties, - resourceSettings.properties + this.filterDefaultOverridableProperties(machineOverridableSettings.properties), + this.filterDefaultOverridableProperties(windowSettings.properties), + this.filterDefaultOverridableProperties(resourceSettings.properties) ), patternProperties: { [OVERRIDE_PROPERTY_PATTERN]: { @@ -1316,6 +1316,16 @@ class RegisterConfigurationSchemasContribution extends Disposable implements IWo }); return result; } + + private filterDefaultOverridableProperties(properties: IStringDictionary): IStringDictionary { + const result: IStringDictionary = {}; + Object.entries(properties).forEach(([key, value]) => { + if (!value.disallowConfigurationDefault) { + result[key] = value; + } + }); + return result; + } } class ResetConfigurationDefaultsOverridesCache extends Disposable implements IWorkbenchContribution { @@ -1365,7 +1375,7 @@ class UpdateExperimentalSettingsDefaults extends Disposable implements IWorkbenc } catch (error) {/*ignore */ } } if (Object.keys(overrides).length) { - this.configurationRegistry.registerDefaultConfigurations([{ overrides, source: localize('experimental', "Experiments") }]); + this.configurationRegistry.registerDefaultConfigurations([{ overrides }]); } } } diff --git a/src/vs/workbench/services/configuration/test/browser/configuration.test.ts b/src/vs/workbench/services/configuration/test/browser/configuration.test.ts index b03e14cb2d55d..5dfe022d2b5e8 100644 --- a/src/vs/workbench/services/configuration/test/browser/configuration.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configuration.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -48,8 +48,7 @@ suite('DefaultConfiguration', () => { teardown(() => { 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('configuration default overrides are read from environment', async () => { diff --git a/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts index 31139bf5737cc..8575a047a795d 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditing.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; import * as json from 'vs/base/common/json'; import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index eab8d6064f58c..bd6c6a3d5ef3a 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/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 * 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/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 9cfc93c8dfec1..bbfd61cdd7d40 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/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 { Registry } from 'vs/platform/registry/common/platform'; import { StandaloneConfigurationModelParser, Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; import { ConfigurationModelParser, ConfigurationModel, ConfigurationParseOptions } from 'vs/platform/configuration/common/configurationModels'; diff --git a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts index de7b63c75a081..a7c716da65d8c 100644 --- a/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts @@ -285,7 +285,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise { if (!inputInfos) { - return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'input'))); + return Promise.reject(new Error(nls.localize('inputVariable.noInputSection', "Variable '{0}' must be defined in an '{1}' section of the debug or task configuration.", variable, 'inputs'))); } // find info for the given input variable diff --git a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts index b776e512ce8b8..c3636300a65f0 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-sandbox/configurationResolverService.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index edf7fb273004c..7453c76c81606 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -245,8 +245,9 @@ export class DecorationsService implements IDecorationsService { declare _serviceBrand: undefined; - private readonly _onDidChangeDecorationsDelayed = new DebounceEmitter({ merge: all => all.flat() }); - private readonly _onDidChangeDecorations = new Emitter(); + private readonly _store = new DisposableStore(); + private readonly _onDidChangeDecorationsDelayed = this._store.add(new DebounceEmitter({ merge: all => all.flat() })); + private readonly _onDidChangeDecorations = this._store.add(new Emitter()); onDidChangeDecorations: Event = this._onDidChangeDecorations.event; @@ -261,12 +262,11 @@ export class DecorationsService implements IDecorationsService { this._decorationStyles = new DecorationStyles(themeService); this._data = TernarySearchTree.forUris(key => uriIdentityService.extUri.ignorePathCasing(key)); - this._onDidChangeDecorationsDelayed.event(event => { this._onDidChangeDecorations.fire(new FileDecorationChangeEvent(event)); }); + this._store.add(this._onDidChangeDecorationsDelayed.event(event => { this._onDidChangeDecorations.fire(new FileDecorationChangeEvent(event)); })); } dispose(): void { - this._onDidChangeDecorations.dispose(); - this._onDidChangeDecorationsDelayed.dispose(); + this._store.dispose(); this._data.clear(); } diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index ae94c92179d9a..e3e9fbb209eef 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { DecorationsService } from 'vs/workbench/services/decorations/browser/decorationsService'; import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/common/decorations'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 5f3b9cbda7de3..95be556e81aa0 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -832,7 +832,7 @@ export class SimpleFileDialog implements ISimpleFileDialog { } else if (!statDirname.isDirectory) { this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateNonexistentDir', 'Please enter a path that exists.'); return Promise.resolve(false); - } else if (statDirname.readonly || statDirname.locked) { + } else if (statDirname.readonly) { this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateReadonlyFolder', 'This folder cannot be used as a save destination. Please choose another folder'); return Promise.resolve(false); } diff --git a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts index 5439e7be42f36..37db54ee4935d 100644 --- a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts +++ b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; @@ -101,7 +101,7 @@ suite('FileDialogService', function () { const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); - const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); + const workspaceService: IWorkspaceEditingService = disposables.add(instantiationService.createInstance(BrowserWorkspaceEditingService)); assert.strictEqual((await workspaceService.pickNewWorkspacePath())?.path.startsWith(testFile.path), true); assert.strictEqual(await dialogService.pickWorkspaceAndOpen({}), undefined); }); @@ -126,7 +126,7 @@ suite('FileDialogService', function () { } as IPathService); const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); - const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); + const workspaceService: IWorkspaceEditingService = disposables.add(instantiationService.createInstance(BrowserWorkspaceEditingService)); assert.strictEqual((await workspaceService.pickNewWorkspacePath())?.path.startsWith(testFile.path), true); assert.strictEqual(await dialogService.pickWorkspaceAndOpen({}), undefined); }); @@ -158,7 +158,7 @@ suite('FileDialogService', function () { } as IPathService); const dialogService = instantiationService.createInstance(TestFileDialogService, new TestSimpleFileDialog()); instantiationService.set(IFileDialogService, dialogService); - const workspaceService: IWorkspaceEditingService = instantiationService.createInstance(BrowserWorkspaceEditingService); + const workspaceService: IWorkspaceEditingService = disposables.add(instantiationService.createInstance(BrowserWorkspaceEditingService)); assert.strictEqual((await workspaceService.pickNewWorkspacePath())?.path.startsWith(testFile.path), true); assert.strictEqual(await dialogService.pickWorkspaceAndOpen({}), undefined); }); diff --git a/src/vs/workbench/services/editor/common/customEditorLabelService.ts b/src/vs/workbench/services/editor/common/customEditorLabelService.ts index 8264adcbd91c0..bd6633067a127 100644 --- a/src/vs/workbench/services/editor/common/customEditorLabelService.ts +++ b/src/vs/workbench/services/editor/common/customEditorLabelService.ts @@ -51,10 +51,10 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito this.storeEnablementState(); this.storeCustomPatterns(); - this.registerListernes(); + this.registerListeners(); } - private registerListernes(): void { + private registerListeners(): void { this._register(this.configurationService.onDidChangeConfiguration(e => { // Cache the enabled state if (e.affectsConfiguration(CustomEditorLabelService.SETTING_ID_ENABLED)) { @@ -148,29 +148,43 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito } if (pattern.parsedPattern(relevantPath)) { - return this.applyTempate(pattern.template, resource, relevantPath); + return this.applyTemplate(pattern.template, resource, relevantPath); } } return undefined; } - private readonly _parsedTemplateExpression = /\$\{(dirname|filename|extname|dirname\(([-+]?\d+)\))\}/g; - private applyTempate(template: string, resource: URI, relevantPath: string): string { + private readonly _parsedTemplateExpression = /\$\{(dirname|filename|extname|extname\((?[-+]?\d+)\)|dirname\((?[-+]?\d+)\))\}/g; + private readonly _filenameCaptureExpression = /(?^\.*[^.]*)/; + private applyTemplate(template: string, resource: URI, relevantPath: string): string { let parsedPath: undefined | ParsedPath; - return template.replace(this._parsedTemplateExpression, (match: string, variable: string, arg: string) => { + return template.replace(this._parsedTemplateExpression, (match: string, variable: string, ...args: any[]) => { parsedPath = parsedPath ?? parsePath(resource.path); - switch (variable) { - case 'filename': - return parsedPath.name; - case 'extname': - return parsedPath.ext.slice(1); - default: { // dirname and dirname(arg) - const n = variable === 'dirname' ? 0 : parseInt(arg); - const nthDir = this.getNthDirname(dirname(relevantPath), n); - if (nthDir) { - return nthDir; - } + // named group matches + const { dirnameN = '0', extnameN = '0' }: { dirnameN?: string; extnameN?: string } = args.pop(); + + if (variable === 'filename') { + const { filename } = this._filenameCaptureExpression.exec(parsedPath.base)?.groups ?? {}; + if (filename) { + return filename; + } + } else if (variable === 'extname') { + const extension = this.getExtnames(parsedPath.base); + if (extension) { + return extension; + } + } else if (variable.startsWith('extname')) { + const n = parseInt(extnameN); + const nthExtname = this.getNthExtname(parsedPath.base, n); + if (nthExtname) { + return nthExtname; + } + } else if (variable.startsWith('dirname')) { + const n = parseInt(dirnameN); + const nthDir = this.getNthDirname(dirname(relevantPath), n); + if (nthDir) { + return nthDir; } } @@ -178,12 +192,36 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito }); } + private removeLeadingDot(path: string): string { + let withoutLeadingDot = path; + while (withoutLeadingDot.startsWith('.')) { + withoutLeadingDot = withoutLeadingDot.slice(1); + } + return withoutLeadingDot; + } + private getNthDirname(path: string, n: number): string | undefined { // grand-parent/parent/filename.ext1.ext2 -> [grand-parent, parent] path = path.startsWith('/') ? path.slice(1) : path; const pathFragments = path.split('/'); - const length = pathFragments.length; + return this.getNthFragment(pathFragments, n); + } + + private getExtnames(fullFileName: string): string { + return this.removeLeadingDot(fullFileName).split('.').slice(1).join('.'); + } + + private getNthExtname(fullFileName: string, n: number): string | undefined { + // file.ext1.ext2.ext3 -> [file, ext1, ext2, ext3] + const extensionNameFragments = this.removeLeadingDot(fullFileName).split('.'); + extensionNameFragments.shift(); // remove the first element which is the file name + + return this.getNthFragment(extensionNameFragments, n); + } + + private getNthFragment(fragments: string[], n: number): string | undefined { + const length = fragments.length; let nth; if (n < 0) { @@ -192,11 +230,11 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito nth = length - n - 1; } - const nthDir = pathFragments[nth]; - if (nthDir === undefined || nthDir === '') { + const nthFragment = fragments[nth]; + if (nthFragment === undefined || nthFragment === '') { return undefined; } - return nthDir; + return nthFragment; } } diff --git a/src/vs/workbench/services/editor/test/browser/customEditorLabelService.test.ts b/src/vs/workbench/services/editor/test/browser/customEditorLabelService.test.ts new file mode 100644 index 0000000000000..343c70585d76b --- /dev/null +++ b/src/vs/workbench/services/editor/test/browser/customEditorLabelService.test.ts @@ -0,0 +1,234 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from 'vs/base/common/uri'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { CustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService'; +import { ITestInstantiationService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +suite('Custom Editor Label Service', () => { + + const disposables = new DisposableStore(); + + setup(() => { }); + + teardown(async () => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + async function createCustomLabelService(instantiationService: ITestInstantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[CustomEditorLabelService, TestConfigurationService, TestServiceAccessor]> { + const configService = new TestConfigurationService(); + await configService.setUserConfiguration(CustomEditorLabelService.SETTING_ID_ENABLED, true); + instantiationService.stub(IConfigurationService, configService); + + const customLabelService = disposables.add(instantiationService.createInstance(CustomEditorLabelService)); + return [customLabelService, configService, instantiationService.createInstance(TestServiceAccessor)]; + } + + async function updatePattern(configService: TestConfigurationService, value: any): Promise { + await configService.setUserConfiguration(CustomEditorLabelService.SETTING_ID_PATTERNS, value); + configService.onDidChangeConfigurationEmitter.fire({ + affectsConfiguration: (key: string) => key === CustomEditorLabelService.SETTING_ID_PATTERNS, + source: ConfigurationTarget.USER, + affectedKeys: new Set(CustomEditorLabelService.SETTING_ID_PATTERNS), + change: { + keys: [], + overrides: [] + } + }); + } + + test('Custom Labels: filename.extname', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**': '${filename}.${extname}' + }); + + const filenames = [ + 'file.txt', + 'file.txt1.tx2', + '.file.txt', + ]; + + for (const filename of filenames) { + const label = customLabelService.getName(URI.file(filename)); + assert.strictEqual(label, filename); + } + + let label = customLabelService.getName(URI.file('file')); + assert.strictEqual(label, 'file.${extname}'); + + label = customLabelService.getName(URI.file('.file')); + assert.strictEqual(label, '.file.${extname}'); + }); + + test('Custom Labels: filename', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**': '${filename}', + }); + + assert.strictEqual(customLabelService.getName(URI.file('file')), 'file'); + assert.strictEqual(customLabelService.getName(URI.file('file.txt')), 'file'); + assert.strictEqual(customLabelService.getName(URI.file('file.txt1.txt2')), 'file'); + assert.strictEqual(customLabelService.getName(URI.file('folder/file.txt1.txt2')), 'file'); + + assert.strictEqual(customLabelService.getName(URI.file('.file')), '.file'); + assert.strictEqual(customLabelService.getName(URI.file('.file.txt')), '.file'); + assert.strictEqual(customLabelService.getName(URI.file('.file.txt1.txt2')), '.file'); + assert.strictEqual(customLabelService.getName(URI.file('folder/.file.txt1.txt2')), '.file'); + }); + + test('Custom Labels: extname(N)', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**/ext/**': '${extname}', + '**/ext0/**': '${extname(0)}', + '**/ext1/**': '${extname(1)}', + '**/ext2/**': '${extname(2)}', + '**/extMinus1/**': '${extname(-1)}', + '**/extMinus2/**': '${extname(-2)}', + }); + + interface IExt { + extname?: string; + ext0?: string; + ext1?: string; + ext2?: string; + extMinus1?: string; + extMinus2?: string; + } + + function assertExtname(filename: string, ext: IExt): void { + assert.strictEqual(customLabelService.getName(URI.file(`test/ext/${filename}`)), ext.extname ?? '${extname}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/ext0/${filename}`)), ext.ext0 ?? '${extname(0)}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/ext1/${filename}`)), ext.ext1 ?? '${extname(1)}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/ext2/${filename}`)), ext.ext2 ?? '${extname(2)}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/extMinus1/${filename}`)), ext.extMinus1 ?? '${extname(-1)}', filename); + assert.strictEqual(customLabelService.getName(URI.file(`test/extMinus2/${filename}`)), ext.extMinus2 ?? '${extname(-2)}', filename); + } + + assertExtname('file.txt', { + extname: 'txt', + ext0: 'txt', + extMinus1: 'txt', + }); + + assertExtname('file.txt1.txt2', { + extname: 'txt1.txt2', + ext0: 'txt2', + ext1: 'txt1', + extMinus1: 'txt1', + extMinus2: 'txt2', + }); + + assertExtname('.file.txt1.txt2', { + extname: 'txt1.txt2', + ext0: 'txt2', + ext1: 'txt1', + extMinus1: 'txt1', + extMinus2: 'txt2', + }); + + assertExtname('.file.txt1.txt2.txt3.txt4', { + extname: 'txt1.txt2.txt3.txt4', + ext0: 'txt4', + ext1: 'txt3', + ext2: 'txt2', + extMinus1: 'txt1', + extMinus2: 'txt2', + }); + + assertExtname('file', {}); + assertExtname('.file', {}); + }); + + test('Custom Labels: dirname(N)', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**': '${dirname},${dirname(0)},${dirname(1)},${dirname(2)},${dirname(-1)},${dirname(-2)}', + }); + + interface IDir { + dirname?: string; + dir0?: string; + dir1?: string; + dir2?: string; + dirMinus1?: string; + dirMinus2?: string; + } + + function assertDirname(path: string, dir: IDir): void { + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[0], dir.dirname ?? '${dirname}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[1], dir.dir0 ?? '${dirname(0)}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[2], dir.dir1 ?? '${dirname(1)}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[3], dir.dir2 ?? '${dirname(2)}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[4], dir.dirMinus1 ?? '${dirname(-1)}', path); + assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[5], dir.dirMinus2 ?? '${dirname(-2)}', path); + } + + assertDirname('folder/file.txt', { + dirname: 'folder', + dir0: 'folder', + dirMinus1: 'folder', + }); + + assertDirname('root/folder/file.txt', { + dirname: 'folder', + dir0: 'folder', + dir1: 'root', + dirMinus1: 'root', + dirMinus2: 'folder', + }); + + assertDirname('root/.folder/file.txt', { + dirname: '.folder', + dir0: '.folder', + dir1: 'root', + dirMinus1: 'root', + dirMinus2: '.folder', + }); + + assertDirname('root/parent/folder/file.txt', { + dirname: 'folder', + dir0: 'folder', + dir1: 'parent', + dir2: 'root', + dirMinus1: 'root', + dirMinus2: 'parent', + }); + + assertDirname('file.txt', {}); + }); + + test('Custom Labels: no pattern match', async () => { + const [customLabelService, configService] = await createCustomLabelService(); + + await updatePattern(configService, { + '**/folder/**': 'folder', + 'file': 'file', + }); + + assert.strictEqual(customLabelService.getName(URI.file('file')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('file.txt')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('file.txt1.txt2')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('folder1/file.txt1.txt2')), undefined); + + assert.strictEqual(customLabelService.getName(URI.file('.file')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('.file.txt')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('.file.txt1.txt2')), undefined); + assert.strictEqual(customLabelService.getName(URI.file('folder1/file.txt1.txt2')), undefined); + }); +}); diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 80455d79ac1aa..e2fd3321cac86 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -3,7 +3,7 @@ * Licensed 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, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, ITestInstantiationService, workbenchTeardown, createEditorParts, TestEditorParts } from 'vs/workbench/test/browser/workbenchTestServices'; import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement, IEditorGroupContextKeyProvider } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities, GroupModelChangeKind, SideBySideEditor, IEditorFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; diff --git a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts index 2e44db0f38c29..59ddcd67c571e 100644 --- a/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorResolverService.test.ts @@ -3,7 +3,7 @@ * Licensed 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/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 7671e480d79bd..ce6d1a08d6cb9 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorActivation, IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index 4d8100c5640ff..b968288e9d23d 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IEditorFactoryRegistry, EditorExtensions, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, TestEditorPart, createEditorPart, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 17c6ac2cb10db..525a29618750c 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionIdentifier, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionIdentifier, IExtension, IExtensionManifest, TargetPlatform, IRelaxedExtensionManifest, parseEnabledApiProposalNames } from 'vs/platform/extensions/common/extensions'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IScannedExtension, IWebExtensionsScannerService, ScanOptions } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { isWeb, Language } from 'vs/base/common/platform'; @@ -99,6 +99,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten private readonly systemExtensionsCacheResource: URI | undefined = undefined; private readonly customBuiltinExtensionsCacheResource: URI | undefined = undefined; private readonly resourcesAccessQueueMap = new ResourceMap>(); + private readonly extensionsEnabledWithApiProposalVersion: string[]; constructor( @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, @@ -123,6 +124,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten // Eventually update caches lifecycleService.when(LifecyclePhase.Eventually).then(() => this.updateCaches()); } + this.extensionsEnabledWithApiProposalVersion = productService.extensionsEnabledWithApiProposalVersion?.map(id => id.toLowerCase()) ?? []; } private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[]; extensionGalleryResources: URI[] }> | undefined; @@ -735,7 +737,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten private async toScannedExtension(webExtension: IWebExtension, isBuiltin: boolean, type: ExtensionType = ExtensionType.User): Promise { const validations: [Severity, string][] = []; - let manifest: IExtensionManifest | undefined = webExtension.manifest; + let manifest: IRelaxedExtensionManifest | undefined = webExtension.manifest; if (!manifest) { try { @@ -766,7 +768,8 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const uuid = (webExtension.metadata)?.id; - validations.push(...validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false)); + const validateApiVersion = this.extensionsEnabledWithApiProposalVersion.includes(webExtension.identifier.id.toLowerCase()); + validations.push(...validateExtensionManifest(this.productService.version, this.productService.date, webExtension.location, manifest, false, validateApiVersion)); let isValid = true; for (const [severity, message] of validations) { if (severity === Severity.Error) { @@ -775,6 +778,10 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } } + if (manifest.enabledApiProposals && validateApiVersion) { + manifest.enabledApiProposals = parseEnabledApiProposalNames([...manifest.enabledApiProposals]); + } + return { identifier: { id: webExtension.identifier.id, uuid: webExtension.identifier.uuid || uuid }, location: webExtension.location, @@ -800,7 +807,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return []; } - private async translateManifest(manifest: IExtensionManifest, nlsURL: ITranslations | URI, fallbackNLS?: ITranslations | URI): Promise { + private async translateManifest(manifest: IExtensionManifest, nlsURL: ITranslations | URI, fallbackNLS?: ITranslations | URI): Promise { try { const translations = URI.isUri(nlsURL) ? await this.getTranslations(nlsURL) : nlsURL; const fallbackTranslations = URI.isUri(fallbackNLS) ? await this.getTranslations(fallbackNLS) : fallbackNLS; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index f6ad46751162c..43b9798d12d96 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -226,6 +226,10 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promise.reject(`Invalid location ${extension.location.toString()}`); } + async resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { + await Promise.allSettled(this.servers.map(server => server.extensionManagementService.resetPinnedStateForAllUserExtensions(pinned))); + } + zip(extension: ILocalExtension): Promise { const server = this.getServer(extension); if (server) { @@ -234,13 +238,6 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promise.reject(`Invalid location ${extension.location.toString()}`); } - unzip(zipLocation: URI): Promise { - return Promises.settled(this.servers - // Filter out web server - .filter(server => server !== this.extensionManagementServerService.webExtensionManagementServer) - .map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation))).then(([extensionIdentifier]) => extensionIdentifier); - } - download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise { if (this.extensionManagementServerService.localExtensionManagementServer) { return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.download(extension, operation, donotVerifySignature); @@ -378,6 +375,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench servers.push(server); } + installOptions = { ...(installOptions || {}), isApplicationScoped: extension.isApplicationScoped }; return Promises.settled(servers.map(server => server.extensionManagementService.installFromGallery(gallery, installOptions))).then(([local]) => local); } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index a68f7115fb68d..1fdc37f9fda19 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -199,7 +199,6 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe } zip(extension: ILocalExtension): Promise { throw new Error('unsupported'); } - unzip(zipLocation: URI): Promise { throw new Error('unsupported'); } getManifest(vsix: URI): Promise { throw new Error('unsupported'); } download(): Promise { throw new Error('unsupported'); } reinstallFromGallery(): Promise { throw new Error('unsupported'); } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 79cf8b0ad4e3e..462c4f05694f4 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -24,6 +24,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { areApiProposalsCompatible } from 'vs/platform/extensions/common/extensionValidator'; export class NativeRemoteExtensionManagementService extends RemoteExtensionManagementService { @@ -105,7 +106,7 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag const location = await this.localExtensionManagementServer.extensionManagementService.download(compatible, installed.filter(i => areSameExtensions(i.identifier, compatible.identifier))[0] ? InstallOperation.Update : InstallOperation.Install, !!installOptions.donotVerifySignature); this.logService.info('Downloaded extension:', compatible.identifier.id, location.path); try { - const local = await super.install(location, installOptions); + const local = await super.install(location, { ...installOptions, keepExisting: true }); this.logService.info(`Successfully installed '${compatible.identifier.id}' extension`); return local; } finally { @@ -134,6 +135,10 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag } if (!compatibleExtension) { + const incompatibleApiProposalsMessages: string[] = []; + if (!areApiProposalsCompatible(extension.properties.enabledApiProposals ?? [], incompatibleApiProposalsMessages)) { + throw new ExtensionManagementError(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 (!includePreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { throw new ExtensionManagementError(localize('notFoundReleaseExtension', "Can't install release version of '{0}' extension because it has no release version.", extension.identifier.id), ExtensionManagementErrorCode.ReleaseVersionNotFound); diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 8f496d54b04cb..8983896971378 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.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 { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, DidUpdateExtensionMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, ExtensionInstallLocation, IProfileAwareExtensionManagementService, DidChangeProfileEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 1611fb028703f..a5596ccb66c33 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -8,7 +8,7 @@ import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; -import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -111,19 +111,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._initFetchFileSystem(); } - protected async _scanSingleExtension(extension: IExtension): Promise { - if (extension.location.scheme === Schemas.vscodeRemote) { - return this._remoteExtensionsScannerService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); - } - - const scannedExtension = await this._webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, this._userDataProfileService.currentProfile.extensionsResource); - if (scannedExtension) { - return toExtensionDescription(scannedExtension); - } - - return null; - } - private _initFetchFileSystem(): void { const provider = new FetchFileSystemProvider(); this._register(this._fileService.registerProvider(Schemas.http, provider)); diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 0c8d2e2731728..8f4dc4a6b57a8 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -181,6 +181,23 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost err.stack = stack; return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err); } + if (event.data.type === 'vscode.bootstrap.nls') { + const factoryModuleId = 'vs/base/worker/workerMain.js'; + const baseUrl = require.toUrl(factoryModuleId).slice(0, -factoryModuleId.length); + iframe.contentWindow!.postMessage({ + type: event.data.type, + data: { + baseUrl, + workerUrl: require.toUrl(factoryModuleId), + nls: { + // VSCODE_GLOBALS: NLS + messages: globalThis._VSCODE_NLS_MESSAGES, + language: globalThis._VSCODE_NLS_LANGUAGE + } + } + }, '*'); + return; + } const { data } = event.data; if (barrier.isOpen() || !(data instanceof MessagePort)) { console.warn('UNEXPECTED message', event); diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 124b22f400b5b..be9bb04b1041e 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -7,7 +7,7 @@ import { Barrier } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as perf from 'vs/base/common/performance'; import { isCI } from 'vs/base/common/platform'; @@ -45,13 +45,13 @@ import { IResolveAuthorityErrorResult } from 'vs/workbench/services/extensions/c import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { ExtensionRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; import { ExtensionRunningLocationTracker, filterExtensionIdentifiers } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker'; -import { ActivationKind, ActivationTimes, ExtensionActivationReason, ExtensionHostStartup, ExtensionPointContribution, IExtensionHost, IExtensionService, IExtensionsStatus, IInternalExtensionService, IMessage, IResponsiveStateChangeEvent, IWillActivateEvent, WillStopExtensionHostsEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, ActivationTimes, ExtensionActivationReason, ExtensionHostStartup, ExtensionPointContribution, IExtensionHost, IExtensionService, IExtensionsStatus, IInternalExtensionService, IMessage, IResponsiveStateChangeEvent, IWillActivateEvent, WillStopExtensionHostsEvent, toExtension, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionsProposedApi } from 'vs/workbench/services/extensions/common/extensionsProposedApi'; import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { LazyCreateExtensionHostManager } from 'vs/workbench/services/extensions/common/lazyCreateExtensionHostManager'; import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; import { IExtensionActivationHost as IWorkspaceContainsActivationHost, checkActivateWorkspaceContainsExtension, checkGlobFileExists } from 'vs/workbench/services/extensions/common/workspaceContains'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ILifecycleService, WillShutdownJoinerOrder } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IExtensionHostExitInfo, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; const hasOwnProperty = Object.hasOwnProperty; @@ -90,7 +90,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private _deltaExtensionsQueue: DeltaExtensionsQueueItem[] = []; private _inHandleDeltaExtensions = false; - private _extensionHostManagers: IExtensionHostManager[] = []; + private readonly _extensionHostManagers = this._register(new ExtensionHostCollection()); private _resolveAuthorityAttempt: number = 0; @@ -195,6 +195,16 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } })); + this._register(this._lifecycleService.onWillShutdown(event => { + if (this._remoteAgentService.getConnection()) { + event.join(() => this._remoteAgentService.endConnection(), { + id: 'join.disconnectRemote', + label: nls.localize('disconnectRemote', "Disconnect Remote Agent"), + order: WillShutdownJoinerOrder.Last // after others have joined that might depend on a remote connection + }); + } + })); + this._register(this._lifecycleService.onDidShutdown(() => { // We need to disconnect the management connection before killing the local extension host. // Otherwise, the local extension host might terminate the underlying tunnel before the @@ -207,16 +217,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } protected _getExtensionHostManagers(kind: ExtensionHostKind): IExtensionHostManager[] { - return this._extensionHostManagers.filter(extHostManager => extHostManager.kind === kind); - } - - private _getExtensionHostManagerByRunningLocation(runningLocation: ExtensionRunningLocation): IExtensionHostManager | null { - for (const extensionHostManager of this._extensionHostManagers) { - if (extensionHostManager.representsRunningLocation(runningLocation)) { - return extensionHostManager; - } - } - return null; + return this._extensionHostManagers.getByKind(kind); } //#region deltaExtensions @@ -278,7 +279,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx for (let i = 0, len = _toAdd.length; i < len; i++) { const extension = _toAdd[i]; - const extensionDescription = await this._scanSingleExtension(extension); + const extensionDescription = toExtensionDescription(extension, false); if (!extensionDescription) { // could not scan extension... continue; @@ -557,7 +558,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } if (runningLocation !== null) { - return this._getExtensionHostManagerByRunningLocation(runningLocation); + return this._extensionHostManagers.getByRunningLocation(runningLocation); } return null; } @@ -662,8 +663,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#region Stopping / Starting / Restarting - public stopExtensionHosts(reason: string): Promise { - return this._doStopExtensionHostsWithVeto(reason); + public stopExtensionHosts(reason: string, auto?: boolean): Promise { + return this._doStopExtensionHostsWithVeto(reason, auto); } protected _doStopExtensionHosts(): void { @@ -674,13 +675,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } - // See https://github.com/microsoft/vscode/issues/152204 - // Dispose extension hosts in reverse creation order because the local extension host - // might be critical in sustaining a connection to the remote extension host - for (let i = this._extensionHostManagers.length - 1; i >= 0; i--) { - this._extensionHostManagers[i].dispose(); - } - this._extensionHostManagers = []; + this._extensionHostManagers.disposeAllInReverse(); for (const extensionStatus of this._extensionStatus.values()) { extensionStatus.clearRuntimeStatus(); } @@ -690,7 +685,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } - private async _doStopExtensionHostsWithVeto(reason: string): Promise { + private async _doStopExtensionHostsWithVeto(reason: string, auto?: boolean): Promise { const vetos: (boolean | Promise)[] = []; const vetoReasons = new Set(); @@ -719,16 +714,18 @@ export abstract class AbstractExtensionService extends Disposable implements IEx if (!veto) { this._doStopExtensionHosts(); } else { - const vetoReasonsArray = Array.from(vetoReasons); - - this._logService.warn(`Extension host was not stopped because of veto (stop reason: ${reason}, veto reason: ${vetoReasonsArray.join(', ')})`); + if (!auto) { + const vetoReasonsArray = Array.from(vetoReasons); + + this._logService.warn(`Extension host was not stopped because of veto (stop reason: ${reason}, veto reason: ${vetoReasonsArray.join(', ')})`); + await this._dialogService.warn( + nls.localize('extensionStopVetoMessage', "The following operation was blocked: {0}", reason), + vetoReasonsArray.length === 1 ? + nls.localize('extensionStopVetoDetailsOne', "The reason for blocking the operation: {0}", vetoReasonsArray[0]) : + nls.localize('extensionStopVetoDetailsMany', "The reasons for blocking the operation:\n- {0}", vetoReasonsArray.join('\n -')), + ); + } - await this._dialogService.warn( - nls.localize('extensionStopVetoMessage', "The following operation was blocked: {0}", reason), - vetoReasonsArray.length === 1 ? - nls.localize('extensionStopVetoDetailsOne', "The reason for blocking the operation: {0}", vetoReasonsArray[0]) : - nls.localize('extensionStopVetoDetailsMany', "The reasons for blocking the operation:\n- {0}", vetoReasonsArray.join('\n -')), - ); } return !veto; @@ -744,26 +741,28 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } locations.push(new RemoteRunningLocation()); for (const location of locations) { - if (this._getExtensionHostManagerByRunningLocation(location)) { + if (this._extensionHostManagers.getByRunningLocation(location)) { // already running continue; } - const extHostManager = this._createExtensionHostManager(location, isInitialStart, initialActivationEvents); - if (extHostManager) { - this._extensionHostManagers.push(extHostManager); + const res = this._createExtensionHostManager(location, isInitialStart, initialActivationEvents); + if (res) { + const [extHostManager, disposableStore] = res; + this._extensionHostManagers.add(extHostManager, disposableStore); } } } - private _createExtensionHostManager(runningLocation: ExtensionRunningLocation, isInitialStart: boolean, initialActivationEvents: string[]): IExtensionHostManager | null { + private _createExtensionHostManager(runningLocation: ExtensionRunningLocation, isInitialStart: boolean, initialActivationEvents: string[]): null | [IExtensionHostManager, DisposableStore] { const extensionHost = this._extensionHostFactory.createExtensionHost(this._runningLocations, runningLocation, isInitialStart); if (!extensionHost) { return null; } const processManager: IExtensionHostManager = this._doCreateExtensionHostManager(extensionHost, initialActivationEvents); - processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal)); - processManager.onDidChangeResponsiveState((responsiveState) => { + const disposableStore = new DisposableStore(); + disposableStore.add(processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal))); + disposableStore.add(processManager.onDidChangeResponsiveState((responsiveState) => { this._logService.info(`Extension host (${processManager.friendyName}) is ${responsiveState === ResponsiveState.Responsive ? 'responsive' : 'unresponsive'}.`); this._onDidChangeResponsiveChange.fire({ extensionHostKind: processManager.kind, @@ -772,8 +771,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return processManager.getInspectPort(tryEnableInspector); } }); - }); - return processManager; + })); + return [processManager, disposableStore]; } protected _doCreateExtensionHostManager(extensionHost: IExtensionHost, initialActivationEvents: string[]): IExtensionHostManager { @@ -804,13 +803,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx if (signal) { this._onRemoteExtensionHostCrashed(extensionHost, signal); } - for (let i = 0; i < this._extensionHostManagers.length; i++) { - if (this._extensionHostManagers[i] === extensionHost) { - this._extensionHostManagers[i].dispose(); - this._extensionHostManagers.splice(i, 1); - break; - } - } + this._extensionHostManagers.disposeOne(extensionHost); } } @@ -1189,11 +1182,85 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#endregion protected abstract _resolveExtensions(): Promise; - protected abstract _scanSingleExtension(extension: IExtension): Promise; protected abstract _onExtensionHostExit(code: number): void; protected abstract _resolveAuthority(remoteAuthority: string): Promise; } +class ExtensionHostCollection extends Disposable { + + private _extensionHostManagers: ExtensionHostManagerData[] = []; + + public override dispose(): void { + this.disposeAllInReverse(); + super.dispose(); + } + + public add(extensionHostManager: IExtensionHostManager, disposableStore: DisposableStore): void { + this._extensionHostManagers.push(new ExtensionHostManagerData(extensionHostManager, disposableStore)); + } + + public disposeAllInReverse(): void { + // See https://github.com/microsoft/vscode/issues/152204 + // Dispose extension hosts in reverse creation order because the local extension host + // might be critical in sustaining a connection to the remote extension host + for (let i = this._extensionHostManagers.length - 1; i >= 0; i--) { + this._extensionHostManagers[i].dispose(); + } + this._extensionHostManagers = []; + } + + public disposeOne(extensionHostManager: IExtensionHostManager): void { + const index = this._extensionHostManagers.findIndex(el => el.extensionHost === extensionHostManager); + if (index >= 0) { + this._extensionHostManagers.splice(index, 1); + extensionHostManager.dispose(); + } + } + + public getByKind(kind: ExtensionHostKind): IExtensionHostManager[] { + return this.filter(el => el.kind === kind); + } + + public getByRunningLocation(runningLocation: ExtensionRunningLocation): IExtensionHostManager | null { + for (const el of this._extensionHostManagers) { + if (el.extensionHost.representsRunningLocation(runningLocation)) { + return el.extensionHost; + } + } + return null; + } + + *[Symbol.iterator]() { + for (const extensionHostManager of this._extensionHostManagers) { + yield extensionHostManager.extensionHost; + } + } + + public map(callback: (extHostManager: IExtensionHostManager) => T): T[] { + return this._extensionHostManagers.map(el => callback(el.extensionHost)); + } + + public every(callback: (extHostManager: IExtensionHostManager) => unknown): boolean { + return this._extensionHostManagers.every(el => callback(el.extensionHost)); + } + + public filter(callback: (extHostManager: IExtensionHostManager) => unknown): IExtensionHostManager[] { + return this._extensionHostManagers.filter(el => callback(el.extensionHost)).map(el => el.extensionHost); + } +} + +class ExtensionHostManagerData { + constructor( + public readonly extensionHost: IExtensionHostManager, + public readonly disposableStore: DisposableStore + ) { } + + public dispose(): void { + this.disposableStore.dispose(); + this.extensionHost.dispose(); + } +} + export class ResolvedExtensions { constructor( public readonly local: IExtensionDescription[], diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index d5cd7e0d28e79..1ca33752d78c5 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -10,12 +10,12 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ImplicitActivationEvents } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, ExtensionType, IExtension, IExtensionContributions, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { ApiProposalName } from 'vs/platform/extensions/common/extensionsApiProposals'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensionHostKind'; import { IExtensionDescriptionDelta, IExtensionDescriptionSnapshot } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation'; -import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export const nullExtensionDescription = Object.freeze({ @@ -28,7 +28,7 @@ export const nullExtensionDescription = Object.freeze({ isBuiltin: false, targetPlatform: TargetPlatform.UNDEFINED, isUserBuiltin: false, - isUnderDevelopment: false, + isUnderDevelopment: false }); export type WebWorkerExtHostConfigValue = boolean | 'auto'; @@ -513,10 +513,12 @@ export interface IExtensionService { * @param reason a human readable reason for stopping the extension hosts. This maybe * can be presented to the user when showing dialogs. * + * @param auto indicates if the operation was triggered by an automatic action + * * @returns a promise that resolves to `true` if the extension hosts were stopped, `false` * if the operation was vetoed by listeners of the `onWillStop` event. */ - stopExtensionHosts(reason: string): Promise; + stopExtensionHosts(reason: string, auto?: boolean): Promise; /** * Starts the extension hosts. If updates are provided, the extension hosts are started with the given updates. @@ -556,16 +558,18 @@ export function toExtension(extensionDescription: IExtensionDescription): IExten } export function toExtensionDescription(extension: IExtension, isUnderDevelopment?: boolean): IExtensionDescription { + const id = getExtensionId(extension.manifest.publisher, extension.manifest.name); return { - identifier: new ExtensionIdentifier(getExtensionId(extension.manifest.publisher, extension.manifest.name)), + id, + identifier: new ExtensionIdentifier(id), isBuiltin: extension.type === ExtensionType.System, isUserBuiltin: extension.type === ExtensionType.User && extension.isBuiltin, isUnderDevelopment: !!isUnderDevelopment, extensionLocation: extension.location, - ...extension.manifest, uuid: extension.identifier.uuid, targetPlatform: extension.targetPlatform, publisherDisplayName: extension.publisherDisplayName, + ...extension.manifest }; } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts deleted file mode 100644 index 28cf88a67b333..0000000000000 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ /dev/null @@ -1,129 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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. - -export const allApiProposals = Object.freeze({ - activeComment: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.activeComment.d.ts', - aiRelatedInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', - aiTextSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', - attributableCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts', - authGetSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', - authLearnMore: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', - authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', - canonicalUriProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', - chatParticipantAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts', - chatParticipantPrivate: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', - chatProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', - chatTab: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', - chatVariableResolver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts', - codeActionAI: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionAI.d.ts', - codeActionRanges: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codeActionRanges.d.ts', - codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', - commentReactor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentReactor.d.ts', - commentThreadApplicability: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts', - commentingRangeHint: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentingRangeHint.d.ts', - commentsDraftState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts', - contribAccessibilityHelpContent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribAccessibilityHelpContent.d.ts', - contribCommentEditorActionsMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', - contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', - contribCommentThreadAdditionalMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', - contribCommentsViewThreadMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentsViewThreadMenus.d.ts', - contribDiffEditorGutterToolBarMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribDiffEditorGutterToolBarMenus.d.ts', - contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', - contribEditorContentMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', - contribIssueReporter: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribIssueReporter.d.ts', - contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', - contribMenuBarHome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts', - contribMergeEditorMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorMenus.d.ts', - contribMultiDiffEditorMenus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMultiDiffEditorMenus.d.ts', - contribNotebookStaticPreloads: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribNotebookStaticPreloads.d.ts', - contribRemoteHelp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', - contribShareMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts', - contribSourceControlHistoryItemGroupMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemGroupMenu.d.ts', - contribSourceControlHistoryItemMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemMenu.d.ts', - contribSourceControlInputBoxMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlInputBoxMenu.d.ts', - contribSourceControlTitleMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribSourceControlTitleMenu.d.ts', - contribStatusBarItems: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribStatusBarItems.d.ts', - contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', - contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', - createFileSystemWatcher: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts', - customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', - debugVisualization: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugVisualization.d.ts', - defaultChatParticipant: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts', - diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', - diffContentOptions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffContentOptions.d.ts', - documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', - documentPaste: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', - editSessionIdentityProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts', - editorHoverVerbosityLevel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts', - editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', - embeddings: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.embeddings.d.ts', - extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', - extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', - externalUriOpener: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts', - fileComments: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileComments.d.ts', - fileSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', - findFiles2: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findFiles2.d.ts', - findTextInFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', - fsChunks: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', - idToken: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', - inlineCompletionsAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', - inlineEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineEdit.d.ts', - interactive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactive.d.ts', - interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', - ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', - languageModelSystem: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModelSystem.d.ts', - languageStatusText: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts', - mappedEditsProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', - multiDocumentHighlightProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts', - newSymbolNamesProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.newSymbolNamesProvider.d.ts', - notebookCellExecution: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecution.d.ts', - notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', - notebookControllerAffinityHidden: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerAffinityHidden.d.ts', - notebookDeprecated: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', - notebookExecution: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookExecution.d.ts', - notebookKernelSource: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts', - notebookLiveShare: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', - notebookMessaging: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', - notebookMime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', - notebookVariableProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookVariableProvider.d.ts', - portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', - profileContentHandlers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts', - quickDiffProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', - quickPickItemTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', - quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', - resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', - scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', - scmHistoryProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts', - scmMultiDiffEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts', - scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', - scmTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmTextDocument.d.ts', - scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', - shareProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.shareProvider.d.ts', - showLocal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.showLocal.d.ts', - speech: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.speech.d.ts', - tabInputMultiDiff: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts', - tabInputTextMerge: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts', - taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', - telemetry: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', - terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', - terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', - terminalExecuteCommandEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalExecuteCommandEvent.d.ts', - terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', - terminalSelection: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalSelection.d.ts', - terminalShellIntegration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts', - testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', - textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', - timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', - tokenInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', - treeViewActiveItem: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', - treeViewMarkdownMessage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewMarkdownMessage.d.ts', - treeViewReveal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewReveal.d.ts', - tunnelFactory: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnelFactory.d.ts', - tunnels: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnels.d.ts', - workspaceTrust: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts' -}); -export type ApiProposalName = keyof typeof allApiProposals; diff --git a/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts b/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts index e92c5e0a50ef3..8133ef3e43a03 100644 --- a/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts +++ b/src/vs/workbench/services/extensions/common/extensionsProposedApi.ts @@ -4,11 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { localize } from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { allApiProposals, ApiProposalName } from 'vs/platform/extensions/common/extensionsApiProposals'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; +import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ApiProposalName, allApiProposals } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; +import { Extensions, IExtensionFeatureMarkdownRenderer, IExtensionFeaturesRegistry, IRenderedData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { Mutable } from 'vs/base/common/types'; export class ExtensionsProposedApi { @@ -54,12 +61,9 @@ export class ExtensionsProposedApi { } } - private doUpdateEnabledApiProposals(_extension: IExtensionDescription): void { + private doUpdateEnabledApiProposals(extension: Mutable): void { - // this is a trick to make the extension description writeable... - type Writeable = { -readonly [P in keyof T]: Writeable }; - const extension = >_extension; - const key = ExtensionIdentifier.toKey(_extension.identifier); + const key = ExtensionIdentifier.toKey(extension.identifier); // warn about invalid proposal and remove them from the list if (isNonEmptyArray(extension.enabledApiProposals)) { @@ -110,3 +114,35 @@ export class ExtensionsProposedApi { } } } + +class ApiProposalsMarkdowneRenderer extends Disposable implements IExtensionFeatureMarkdownRenderer { + + readonly type = 'markdown'; + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.originalEnabledApiProposals?.length || !!manifest.enabledApiProposals?.length; + } + + render(manifest: IExtensionManifest): IRenderedData { + const enabledApiProposals = manifest.originalEnabledApiProposals ?? manifest.enabledApiProposals ?? []; + const data = new MarkdownString(); + if (enabledApiProposals.length) { + for (const proposal of enabledApiProposals) { + data.appendMarkdown(`- \`${proposal}\`\n`); + } + } + return { + data, + dispose: () => { } + }; + } +} + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'enabledApiProposals', + label: localize('enabledProposedAPIs', "API Proposals"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ApiProposalsMarkdowneRenderer), +}); diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index f39fd30b602f5..b5ef3e6c9d593 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -13,10 +13,10 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IMessage } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionDescription, EXTENSION_CATEGORIES, ExtensionIdentifierSet } from 'vs/platform/extensions/common/extensions'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; -import { allApiProposals } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { productSchemaId } from 'vs/platform/product/common/productService'; import { ImplicitActivationEvents, IActivationEventsGenerator } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { allApiProposals } from 'vs/platform/extensions/common/extensionsApiProposals'; const schemaRegistry = Registry.as(Extensions.JSONContribution); @@ -242,8 +242,8 @@ export const schema: IJSONSchema = { uniqueItems: true, items: { type: 'string', - enum: Object.keys(allApiProposals), - markdownEnumDescriptions: Object.values(allApiProposals) + enum: Object.keys(allApiProposals).map(proposalName => proposalName), + markdownEnumDescriptions: Object.values(allApiProposals).map(value => value.proposal) } }, api: { @@ -385,6 +385,16 @@ export const schema: IJSONSchema = { body: 'onIssueReporterOpened', description: nls.localize('vscode.extension.activationEvents.onIssueReporterOpened', 'An activation event emitted when the issue reporter is opened.'), }, + { + label: 'onChatParticipant', + body: 'onChatParticipant:${1:participantId}', + description: nls.localize('vscode.extension.activationEvents.onChatParticipant', 'An activation event emitted when the specified chat participant is invoked.'), + }, + { + label: 'onLanguageModelTool', + body: 'onLanguageModelTool:${1:toolName}', + description: nls.localize('vscode.extension.activationEvents.onLanguageModelTool', 'An activation event emitted when the specified language model tool is invoked.'), + }, { label: '*', description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'), @@ -652,7 +662,7 @@ schemaRegistry.registerSchema(productSchemaId, { items: { type: 'string', enum: Object.keys(allApiProposals), - markdownEnumDescriptions: Object.values(allApiProposals) + markdownEnumDescriptions: Object.values(allApiProposals).map(value => value.proposal) } }] } diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index a1d2090c747bc..71d384aaa9d95 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionIdentifierMap, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifierMap, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { localize } from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; import * as semver from 'vs/base/common/semver/semver'; +import { Mutable } from 'vs/base/common/types'; // TODO: @sandy081 merge this with deduping in extensionsScannerService.ts export function dedupExtensions(system: IExtensionDescription[], user: IExtensionDescription[], workspace: IExtensionDescription[], development: IExtensionDescription[], logService: ILogService): IExtensionDescription[] { @@ -27,7 +28,7 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio return; } // Overwriting a builtin extension inherits the `isBuiltin` property and it doesn't show a warning - (userExtension).isBuiltin = true; + (>userExtension).isBuiltin = true; } else { logService.warn(localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); } @@ -50,7 +51,7 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio if (extension) { if (extension.isBuiltin) { // Overwriting a builtin extension inherits the `isBuiltin` property - (developedExtension).isBuiltin = true; + (>developedExtension).isBuiltin = true; } } result.set(developedExtension.identifier, developedExtension); diff --git a/src/vs/workbench/services/extensions/common/rpcProtocol.ts b/src/vs/workbench/services/extensions/common/rpcProtocol.ts index c7fbf71b0b7a3..e936f43f09fb6 100644 --- a/src/vs/workbench/services/extensions/common/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/common/rpcProtocol.ts @@ -158,7 +158,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol { this._unacknowledgedCount = 0; this._unresponsiveTime = 0; this._asyncCheckUresponsive = this._register(new RunOnceScheduler(() => this._checkUnresponsive(), 1000)); - this._protocol.onMessage((msg) => this._receiveOneMessage(msg)); + this._register(this._protocol.onMessage((msg) => this._receiveOneMessage(msg))); } public override dispose(): void { diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 681138c04815f..aaf716dc2bc66 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; -import { URI } from 'vs/base/common/uri'; -import { IExtensionDescription, ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription, IExtension } from 'vs/platform/extensions/common/extensions'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IExtensionsScannerService, IScannedExtension, toExtensionDescription as toExtensionDescriptionFromScannedExtension } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -42,11 +40,6 @@ export class CachedExtensionScanner { }); } - public async scanSingleExtension(extensionPath: string, isBuiltin: boolean): Promise { - const scannedExtension = await this._extensionsScannerService.scanExistingExtension(URI.file(path.resolve(extensionPath)), isBuiltin ? ExtensionType.System : ExtensionType.User, { language: platform.language }); - return scannedExtension ? toExtensionDescriptionFromScannedExtension(scannedExtension, false) : null; - } - public async startScanningExtensions(): Promise { try { const extensions = await this._scanInstalledExtensions(); diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index 0074c58d815cf..911ae77eab687 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -281,7 +281,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { // Lifecycle - this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal)); + this._toDispose.add(this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal))); // Notify debugger that we are ready to attach to the process if we run a development extension if (portNumber) { diff --git a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index cdd507e6fffbb..b7b77b23e0ed6 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -19,7 +19,7 @@ import { ConfigurationScope } from 'vs/platform/configuration/common/configurati import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -141,14 +141,6 @@ export class NativeExtensionService extends AbstractExtensionService implements }); } - protected _scanSingleExtension(extension: IExtension): Promise { - if (extension.location.scheme === Schemas.vscodeRemote) { - return this._remoteExtensionsScannerService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); - } - - return this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System); - } - private async _scanAllLocalExtensions(): Promise { return this._extensionScanner.scannedExtensions; } diff --git a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index 3ced43a5f42ae..69c2463958c5f 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -3,7 +3,7 @@ * Licensed 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'; @@ -12,7 +12,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { ExtensionKind, IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionIdentifier, IExtension, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TestInstantiationService, createServices } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -209,7 +209,7 @@ suite('ExtensionService', () => { protected _resolveExtensions(): Promise { throw new Error('Method not implemented.'); } - protected _scanSingleExtension(extension: IExtension): Promise | null> { + protected _scanSingleExtension(extension: IExtension): Promise { throw new Error('Method not implemented.'); } protected _onExtensionHostExit(code: number): void { diff --git a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts index ae0a893194472..f51e36b151a3f 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionStorageMigration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; diff --git a/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts b/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts index 97a4d42235255..603894a3eabed 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts @@ -3,7 +3,7 @@ * Licensed 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'; @@ -44,7 +44,8 @@ suite('ExtensionDescriptionRegistry', () => { activationEvents, main: 'index.js', targetPlatform: TargetPlatform.UNDEFINED, - extensionDependencies: [] + extensionDependencies: [], + enabledApiProposals: undefined, }; } }); diff --git a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts index ab5a069fe14ee..52d7abfebcd20 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { isWeb } from 'vs/base/common/platform'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts index e351f2485fe90..cffda52a23832 100644 --- a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts +++ b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts @@ -3,7 +3,7 @@ * Licensed 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, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; diff --git a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html index 6a16dd10210cb..52045f22ae40d 100644 --- a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html +++ b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html @@ -4,7 +4,7 @@ @@ -66,13 +66,44 @@ } function start() { + + // Before we can load the worker, we need to get the current set of NLS + // configuration into this iframe. We ask the parent window to send it + // together with the necessary information to load the worker via Blob. + + const bootstrapNlsType = 'vscode.bootstrap.nls'; + + self.onmessage = (event) => { + if (event.origin !== parentOrigin || event.data.type !== bootstrapNlsType) { + return; + } + const { data } = event.data; + createWorker(data.baseUrl, data.workerUrl, data.nls.messages, data.nls.language); + }; + + window.parent.postMessage({ + vscodeWebWorkerExtHostId, + type: bootstrapNlsType + }, '*'); + } + + function createWorker(baseUrl, workerUrl, nlsMessages, nlsLanguage) { try { - let workerUrl = '../../../../base/worker/workerMain.js'; if (globalThis.crossOriginIsolated) { workerUrl += '?vscode-coi=2'; // COEP } - const worker = new Worker(workerUrl, { name }); + const blob = new Blob([[ + `/*extensionHostWorker*/`, + `globalThis.MonacoEnvironment = { baseUrl: '${baseUrl}' };`, + // VSCODE_GLOBALS: NLS + `globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(nlsMessages)};`, + `globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(nlsLanguage)};`, + `importScripts('${workerUrl}');`, + `/*extensionHostWorker*/` + ].join('')], { type: 'application/javascript' }); + + const worker = new Worker(URL.createObjectURL(blob), { name }); worker.postMessage('vs/workbench/api/worker/extensionHostWorker'); const nestedWorkers = new Map(); diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index d4c2c6f025ef6..6a3c782492a20 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -125,7 +125,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi private static readonly READONLY_MESSAGES = { providerReadonly: { value: localize('providerReadonly', "Editor is read-only because the file system of the file is read-only."), isTrusted: true }, sessionReadonly: { value: localize({ key: 'sessionReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only in this session. [Click here](command:{0}) to set writeable.", 'workbench.action.files.setActiveEditorWriteableInSession'), isTrusted: true }, - configuredReadonly: { value: localize({ key: 'configuredReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only via settings. [Click here](command:{0}) to configure.", `workbench.action.openSettings?${encodeURIComponent('["files.readonly"]')}`), isTrusted: true }, + configuredReadonly: { value: localize({ key: 'configuredReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only via settings. [Click here](command:{0}) to configure or [toggle for this session](command:{1}).", `workbench.action.openSettings?${encodeURIComponent('["files.readonly"]')}`, 'workbench.action.files.toggleActiveEditorReadonlyInSession'), isTrusted: true }, fileLocked: { value: localize({ key: 'fileLocked', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because of file permissions. [Click here](command:{0}) to set writeable anyway.", 'workbench.action.files.setActiveEditorWriteableInSession'), isTrusted: true }, fileReadonly: { value: localize('fileReadonly', "Editor is read-only because the file is read-only."), isTrusted: true } }; @@ -143,7 +143,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi readonly onDidChangeReadonly = this._onDidChangeReadonly.event; private currentGlobalAutoSaveConfiguration: IAutoSaveConfiguration; - private currentFilesAssociationConfiguration: IStringDictionary; + private currentFilesAssociationConfiguration: IStringDictionary | undefined; private currentHotExitConfiguration: string; private readonly autoSaveConfigurationCache = new LRUCache(1000); @@ -317,7 +317,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi return this.currentGlobalAutoSaveConfiguration; } - private computeAutoSaveConfiguration(resource: URI | undefined, filesConfiguration: IFilesConfigurationNode): ICachedAutoSaveConfiguration { + private computeAutoSaveConfiguration(resource: URI | undefined, filesConfiguration: IFilesConfigurationNode | undefined): ICachedAutoSaveConfiguration { let autoSave: 'afterDelay' | 'onFocusChange' | 'onWindowChange' | undefined; let autoSaveDelay: number | undefined; let autoSaveWorkspaceFilesOnly: boolean | undefined; @@ -326,10 +326,10 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi let isOutOfWorkspace: boolean | undefined; let isShortAutoSaveDelay: boolean | undefined; - switch (filesConfiguration.autoSave ?? FilesConfigurationService.DEFAULT_AUTO_SAVE_MODE) { + switch (filesConfiguration?.autoSave ?? FilesConfigurationService.DEFAULT_AUTO_SAVE_MODE) { case AutoSaveConfiguration.AFTER_DELAY: { autoSave = 'afterDelay'; - autoSaveDelay = typeof filesConfiguration.autoSaveDelay === 'number' && filesConfiguration.autoSaveDelay >= 0 ? filesConfiguration.autoSaveDelay : FilesConfigurationService.DEFAULT_AUTO_SAVE_DELAY; + autoSaveDelay = typeof filesConfiguration?.autoSaveDelay === 'number' && filesConfiguration.autoSaveDelay >= 0 ? filesConfiguration.autoSaveDelay : FilesConfigurationService.DEFAULT_AUTO_SAVE_DELAY; isShortAutoSaveDelay = autoSaveDelay <= FilesConfigurationService.DEFAULT_AUTO_SAVE_DELAY; break; } @@ -343,7 +343,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi break; } - if (filesConfiguration.autoSaveWorkspaceFilesOnly === true) { + if (filesConfiguration?.autoSaveWorkspaceFilesOnly === true) { autoSaveWorkspaceFilesOnly = true; if (resource && !this.contextService.isInsideWorkspace(resource)) { @@ -352,7 +352,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi } } - if (filesConfiguration.autoSaveWhenNoErrors === true) { + if (filesConfiguration?.autoSaveWhenNoErrors === true) { autoSaveWhenNoErrors = true; isShortAutoSaveDelay = undefined; // this configuration disables short auto save delay } diff --git a/src/vs/workbench/services/history/test/browser/historyService.test.ts b/src/vs/workbench/services/history/test/browser/historyService.test.ts index 06478095336ca..e00cd8ae218eb 100644 --- a/src/vs/workbench/services/history/test/browser/historyService.test.ts +++ b/src/vs/workbench/services/history/test/browser/historyService.test.ts @@ -3,7 +3,7 @@ * Licensed 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, toResource } from 'vs/base/test/common/utils'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, createEditorPart, registerTestFileEditor, TestServiceAccessor, TestTextFileEditor, workbenchTeardown, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts index 0e2778b6191fa..7c3e693e96dbe 100644 --- a/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/browserKeyboardMapper.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 'vs/workbench/services/keybinding/browser/keyboardLayouts/en.darwin'; import 'vs/workbench/services/keybinding/browser/keyboardLayouts/de.darwin'; import { KeyboardLayoutContribution } from 'vs/workbench/services/keybinding/browser/keyboardLayouts/_.contribution'; diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts index 34591dbfc3582..e2064decab99a 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts @@ -3,7 +3,7 @@ * Licensed 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 json from 'vs/base/common/json'; import { KeyCode } from 'vs/base/common/keyCodes'; import { KeyCodeChord } from 'vs/base/common/keybindings'; diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts index cfc697b817e8d..3f7687029a902 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingIO.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingIO.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, ScanCode } from 'vs/base/common/keyCodes'; import { KeyCodeChord, decodeKeybinding, ScanCodeChord, Keybinding } from 'vs/base/common/keybindings'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; diff --git a/src/vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts b/src/vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts index 5ee442c7285e1..909109ade3ab5 100644 --- a/src/vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.ts +++ b/src/vs/workbench/services/keybinding/test/node/keyboardMapperTestUtils.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 * as path from 'vs/base/common/path'; import { SingleModifierChord, ResolvedKeybinding, Keybinding } from 'vs/base/common/keybindings'; import { Promises } from 'vs/base/node/pfs'; @@ -46,7 +47,7 @@ export function assertResolveKeybinding(mapper: IKeyboardMapper, keybinding: Key } export function readRawMapping(file: string): Promise { - return Promises.readFile(FileAccess.asFileUri(`vs/workbench/services/keybinding/test/node/${file}.js`).fsPath).then((buff) => { + return fs.promises.readFile(FileAccess.asFileUri(`vs/workbench/services/keybinding/test/node/${file}.js`).fsPath).then((buff) => { const contents = buff.toString(); const func = new Function('define', contents);// CodeQL [SM01632] This is used in tests and we read the files as JS to avoid slowing down TS compilation let rawMappings: T | null = null; @@ -60,7 +61,7 @@ export function readRawMapping(file: string): Promise { export function assertMapping(writeFileIfDifferent: boolean, mapper: IKeyboardMapper, file: string): Promise { const filePath = path.normalize(FileAccess.asFileUri(`vs/workbench/services/keybinding/test/node/${file}`).fsPath); - return Promises.readFile(filePath).then((buff) => { + return fs.promises.readFile(filePath).then((buff) => { const expected = buff.toString().replace(/\r\n/g, '\n'); const actual = mapper.dumpDebugInfo().replace(/\r\n/g, '\n'); if (actual !== expected && writeFileIfDifferent) { diff --git a/src/vs/workbench/services/keybinding/test/node/macLinuxKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/node/macLinuxKeyboardMapper.test.ts index 05a8db5fc738c..7ca4e5a54fc6a 100644 --- a/src/vs/workbench/services/keybinding/test/node/macLinuxKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/node/macLinuxKeyboardMapper.test.ts @@ -3,7 +3,7 @@ * Licensed 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, ScanCode, ScanCodeUtils } from 'vs/base/common/keyCodes'; import { KeyCodeChord, decodeKeybinding, createSimpleKeybinding, ScanCodeChord, Keybinding } from 'vs/base/common/keybindings'; import { UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels'; diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index f5098c5df7250..84afcb96d3797 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as resources from 'vs/base/common/resources'; -import * as assert from 'assert'; +import assert from 'assert'; import { TestEnvironmentService, TestLifecycleService, TestPathService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; diff --git a/src/vs/workbench/services/language/common/languageService.ts b/src/vs/workbench/services/language/common/languageService.ts index a5644c2b309ff..f79548354bf5d 100644 --- a/src/vs/workbench/services/language/common/languageService.ts +++ b/src/vs/workbench/services/language/common/languageService.ts @@ -23,6 +23,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { index } from 'vs/base/common/arrays'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { isString } from 'vs/base/common/types'; export interface IRawLanguageExtensionPoint { id: string; @@ -147,6 +148,10 @@ class LanguageTableRenderer extends Disposable implements IExtensionFeatureTable const grammars = contributes?.grammars || []; grammars.forEach(grammar => { + if (!isString(grammar.language)) { + // ignore the grammars that are only used as includes in other grammars + return; + } let language = byId[grammar.language]; if (language) { @@ -160,6 +165,10 @@ class LanguageTableRenderer extends Disposable implements IExtensionFeatureTable const snippets = contributes?.snippets || []; snippets.forEach(snippet => { + if (!isString(snippet.language)) { + // ignore invalid snippets + return; + } let language = byId[snippet.language]; if (language) { @@ -291,7 +300,7 @@ export class WorkbenchLanguageService extends LanguageService { // Register based on settings if (configuration.files?.associations) { Object.keys(configuration.files.associations).forEach(pattern => { - const langId = configuration.files.associations[pattern]; + const langId = configuration.files!.associations[pattern]; if (typeof langId !== 'string') { this.logService.warn(`Ignoring configured 'files.associations' for '${pattern}' because its type is not a string but '${typeof langId}'`); diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 3357adabc8228..43e124fa05f5e 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -70,7 +70,12 @@ export const enum EditorActionsLocation { export const enum Position { LEFT, RIGHT, - BOTTOM + BOTTOM, + TOP +} + +export function isHorizontal(position: Position): boolean { + return position === Position.BOTTOM || position === Position.TOP; } export const enum PanelOpensMaximizedOptions { @@ -86,6 +91,7 @@ export function positionToString(position: Position): string { case Position.LEFT: return 'left'; case Position.RIGHT: return 'right'; case Position.BOTTOM: return 'bottom'; + case Position.TOP: return 'top'; default: return 'bottom'; } } @@ -93,7 +99,8 @@ export function positionToString(position: Position): string { const positionsByString: { [key: string]: Position } = { [positionToString(Position.LEFT)]: Position.LEFT, [positionToString(Position.RIGHT)]: Position.RIGHT, - [positionToString(Position.BOTTOM)]: Position.BOTTOM + [positionToString(Position.BOTTOM)]: Position.BOTTOM, + [positionToString(Position.TOP)]: Position.TOP }; export function positionFromString(str: string): Position { @@ -318,7 +325,7 @@ export function shouldShowCustomTitleBar(configurationService: IConfigurationSer } if (zenModeActive) { - return false; + return !configurationService.getValue(ZenModeSettings.FULLSCREEN); } const inFullscreen = isFullscreen(window); diff --git a/src/vs/workbench/services/lifecycle/common/lifecycle.ts b/src/vs/workbench/services/lifecycle/common/lifecycle.ts index 7a795423d5dc5..ec5c8098e4091 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycle.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycle.ts @@ -65,9 +65,34 @@ export interface BeforeShutdownErrorEvent { readonly error: Error; } +export enum WillShutdownJoinerOrder { + + /** + * Joiners to run before the `Last` joiners. This is the default order and best for + * most cases. You can be sure that services are still functional at this point. + */ + Default = 1, + + /** + * The joiners to run last. This should ONLY be used in rare cases when you have no + * dependencies to workbench services or state. The workbench may be in a state where + * resources can no longer be accessed or changed. + */ + Last +} + export interface IWillShutdownEventJoiner { - id: string; - label: string; + readonly id: string; + readonly label: string; + readonly order?: WillShutdownJoinerOrder; +} + +export interface IWillShutdownEventDefaultJoiner extends IWillShutdownEventJoiner { + readonly order?: WillShutdownJoinerOrder.Default; +} + +export interface IWillShutdownEventLastJoiner extends IWillShutdownEventJoiner { + readonly order: WillShutdownJoinerOrder.Last; } /** @@ -95,10 +120,21 @@ export interface WillShutdownEvent { * Allows to join the shutdown. The promise can be a long running operation but it * will block the application from closing. * + * @param promise the promise to join the shutdown event. + * @param joiner to identify the join operation in case it takes very long or never + * completes. + */ + join(promise: Promise, joiner: IWillShutdownEventDefaultJoiner): void; + + /** + * Allows to join the shutdown at the end. The promise can be a long running operation but it + * will block the application from closing. + * + * @param promiseFn the promise to join the shutdown event. * @param joiner to identify the join operation in case it takes very long or never * completes. */ - join(promise: Promise, joiner: IWillShutdownEventJoiner): void; + join(promiseFn: (() => Promise), joiner: IWillShutdownEventLastJoiner): void; /** * Allows to access the joiners that have not finished joining this event. diff --git a/src/vs/workbench/services/lifecycle/common/lifecycleService.ts b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts index 09eee47815500..92006c0a5777d 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from 'vs/base/common/event'; import { Barrier } from 'vs/base/common/async'; +import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ILifecycleService, WillShutdownEvent, StartupKind, LifecyclePhase, LifecyclePhaseToString, ShutdownReason, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { ILogService } from 'vs/platform/log/common/log'; import { mark } from 'vs/base/common/performance'; +import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { BeforeShutdownErrorEvent, ILifecycleService, InternalBeforeShutdownEvent, LifecyclePhase, LifecyclePhaseToString, ShutdownReason, StartupKind, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; export abstract class AbstractLifecycleService extends Disposable implements ILifecycleService { @@ -44,7 +44,7 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi constructor( @ILogService protected readonly logService: ILogService, - @IStorageService protected readonly storageService: IStorageService + @IStorageService protected readonly storageService: IStorageService, ) { super(); diff --git a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts index 9d4d9418201c6..90da4ac5afa12 100644 --- a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts @@ -4,26 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; -import { ShutdownReason, ILifecycleService, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService, IWillShutdownEventJoiner, WillShutdownJoinerOrder } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycleService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { INativeHostService } from 'vs/platform/native/common/native'; -import { Promises, disposableTimeout, raceCancellation } from 'vs/base/common/async'; +import { Promises, disposableTimeout, raceCancellation, timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; export class NativeLifecycleService extends AbstractLifecycleService { private static readonly BEFORE_SHUTDOWN_WARNING_DELAY = 5000; private static readonly WILL_SHUTDOWN_WARNING_DELAY = 800; + private static readonly MAX_GRACEFUL_REMOTE_DISCONNECT_TIME = 3000; constructor( @INativeHostService private readonly nativeHostService: INativeHostService, @IStorageService storageService: IStorageService, - @ILogService logService: ILogService + @ILogService logService: ILogService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, ) { super(logService, storageService); @@ -66,6 +69,10 @@ export class NativeLifecycleService extends AbstractLifecycleService { // trigger onWillShutdown events and joining await this.handleWillShutdown(reply.reason); + // now that all services have stored their data, it's safe to terminate + // the remote connection gracefully before synchronously saying we're shut down + await this.handleRemoteAgentDisconnect(); + // trigger onDidShutdown event now that we know we will quit this._onDidShutdown.fire(); @@ -74,6 +81,19 @@ export class NativeLifecycleService extends AbstractLifecycleService { }); } + private async handleRemoteAgentDisconnect(): Promise { + const longRunningWarning = disposableTimeout(() => { + this.logService.warn(`[lifecycle] the remote agent is taking a long time to disconnect, waiting...`); + }, NativeLifecycleService.BEFORE_SHUTDOWN_WARNING_DELAY); + + await Promise.race([ + this.remoteAgentService.endConnection(), + timeout(NativeLifecycleService.MAX_GRACEFUL_REMOTE_DISCONNECT_TIME), + ]); + + longRunningWarning.dispose(); + } + protected async handleBeforeShutdown(reason: ShutdownReason): Promise { const logService = this.logService; @@ -155,19 +175,24 @@ export class NativeLifecycleService extends AbstractLifecycleService { protected async handleWillShutdown(reason: ShutdownReason): Promise { const joiners: Promise[] = []; + const lastJoiners: (() => Promise)[] = []; const pendingJoiners = new Set(); const cts = new CancellationTokenSource(); - this._onWillShutdown.fire({ reason, token: cts.token, joiners: () => Array.from(pendingJoiners.values()), - join(promise, joiner) { - joiners.push(promise); - - // Track promise completion + join(promiseOrPromiseFn, joiner) { pendingJoiners.add(joiner); - promise.finally(() => pendingJoiners.delete(joiner)); + + if (joiner.order === WillShutdownJoinerOrder.Last) { + const promiseFn = typeof promiseOrPromiseFn === 'function' ? promiseOrPromiseFn : () => promiseOrPromiseFn; + lastJoiners.push(() => promiseFn().finally(() => pendingJoiners.delete(joiner))); + } else { + const promise = typeof promiseOrPromiseFn === 'function' ? promiseOrPromiseFn() : promiseOrPromiseFn; + promise.finally(() => pendingJoiners.delete(joiner)); + joiners.push(promise); + } }, force: () => { cts.dispose(true); @@ -181,10 +206,16 @@ export class NativeLifecycleService extends AbstractLifecycleService { try { await raceCancellation(Promises.settled(joiners), cts.token); } catch (error) { - this.logService.error(`[lifecycle]: Error during will-shutdown phase (error: ${toErrorMessage(error)})`); // this error will not prevent the shutdown - } finally { - longRunningWillShutdownWarning.dispose(); + this.logService.error(`[lifecycle]: Error during will-shutdown phase in default joiners (error: ${toErrorMessage(error)})`); } + + try { + await raceCancellation(Promises.settled(lastJoiners.map(lastJoiner => lastJoiner())), cts.token); + } catch (error) { + this.logService.error(`[lifecycle]: Error during will-shutdown phase in last joiners (error: ${toErrorMessage(error)})`); + } + + longRunningWillShutdownWarning.dispose(); } shutdown(): Promise { diff --git a/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts b/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts index 948741f9d0ab1..66d4f8b3c7157 100644 --- a/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.test.ts +++ b/src/vs/workbench/services/lifecycle/test/electron-sandbox/lifecycleService.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 { timeout } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ShutdownReason, WillShutdownJoinerOrder } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { NativeLifecycleService } from 'vs/workbench/services/lifecycle/electron-sandbox/lifecycleService'; import { workbenchInstantiationService } from 'vs/workbench/test/electron-sandbox/workbenchTestServices'; @@ -155,5 +157,34 @@ suite('Lifecycleservice', function () { assert.strictEqual(joinCalled, true); }); + test('onWillShutdown - join order', async function () { + return runWithFakedTimers({ useFakeTimers: true }, async () => { + const order: string[] = []; + + disposables.add(lifecycleService.onWillShutdown(e => { + e.join(async () => { + order.push('disconnect start'); + await timeout(1); + order.push('disconnect end'); + }, { id: 'test', label: 'test', order: WillShutdownJoinerOrder.Last }); + + e.join((async () => { + order.push('default start'); + await timeout(1); + order.push('default end'); + })(), { id: 'test', label: 'test', order: WillShutdownJoinerOrder.Default }); + })); + + await lifecycleService.testHandleWillShutdown(ShutdownReason.QUIT); + + assert.deepStrictEqual(order, [ + 'default start', + 'default end', + 'disconnect start', + 'disconnect end' + ]); + }); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/localization/browser/localeService.ts b/src/vs/workbench/services/localization/browser/localeService.ts index 0d9c51c72cfb6..07616fa0a2e55 100644 --- a/src/vs/workbench/services/localization/browser/localeService.ts +++ b/src/vs/workbench/services/localization/browser/localeService.ts @@ -14,11 +14,69 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { CancellationToken } from 'vs/base/common/cancellation'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; +import { getCookieValue } from 'vs/base/browser/dom'; + +const localeStorage = new class LocaleStorage { + + private static readonly LOCAL_STORAGE_LOCALE_KEY = 'vscode.nls.locale'; + private static readonly LOCAL_STORAGE_EXTENSION_ID_KEY = 'vscode.nls.languagePackExtensionId'; + + constructor() { + this.migrateCookie(); // TODO@bpasero remove me eventually + } + + private migrateCookie(): void { + const localeCookieValue = getCookieValue(LocaleStorage.LOCAL_STORAGE_LOCALE_KEY); + const localeStorageValue = localStorage.getItem(LocaleStorage.LOCAL_STORAGE_LOCALE_KEY); + + if ( + (typeof localeCookieValue !== 'string' && typeof localeStorageValue !== 'string') || + (localeCookieValue === localeStorageValue) + ) { + return; // already matching + } + + if (typeof localeStorageValue === 'string') { + this.doSetLocaleToCookie(localeStorageValue); + } else { + this.doClearLocaleToCookie(); + } + } + + setLocale(locale: string): void { + localStorage.setItem(LocaleStorage.LOCAL_STORAGE_LOCALE_KEY, locale); + this.doSetLocaleToCookie(locale); + } + + private doSetLocaleToCookie(locale: string): void { + document.cookie = `${LocaleStorage.LOCAL_STORAGE_LOCALE_KEY}=${locale};path=/;max-age=3153600000`; + } + + clearLocale(): void { + localStorage.removeItem(LocaleStorage.LOCAL_STORAGE_LOCALE_KEY); + this.doClearLocaleToCookie(); + } + + private doClearLocaleToCookie(): void { + document.cookie = `${LocaleStorage.LOCAL_STORAGE_LOCALE_KEY}=;path=/;max-age=0`; + } + + setExtensionId(extensionId: string): void { + localStorage.setItem(LocaleStorage.LOCAL_STORAGE_EXTENSION_ID_KEY, extensionId); + } + + getExtensionId(): string | null { + return localStorage.getItem(LocaleStorage.LOCAL_STORAGE_EXTENSION_ID_KEY); + } + + clearExtensionId(): void { + localStorage.removeItem(LocaleStorage.LOCAL_STORAGE_EXTENSION_ID_KEY); + } +}; export class WebLocaleService implements ILocaleService { + declare readonly _serviceBrand: undefined; - static readonly _LOCAL_STORAGE_EXTENSION_ID_KEY = 'vscode.nls.languagePackExtensionId'; - static readonly _LOCAL_STORAGE_LOCALE_KEY = 'vscode.nls.locale'; constructor( @IDialogService private readonly dialogService: IDialogService, @@ -32,13 +90,13 @@ export class WebLocaleService implements ILocaleService { return; } if (locale) { - localStorage.setItem(WebLocaleService._LOCAL_STORAGE_LOCALE_KEY, locale); + localeStorage.setLocale(locale); if (languagePackItem.extensionId) { - localStorage.setItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY, languagePackItem.extensionId); + localeStorage.setExtensionId(languagePackItem.extensionId); } } else { - localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_LOCALE_KEY); - localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY); + localeStorage.clearLocale(); + localeStorage.clearExtensionId(); } const restartDialog = await this.dialogService.confirm({ @@ -54,8 +112,8 @@ export class WebLocaleService implements ILocaleService { } async clearLocalePreference(): Promise { - localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_LOCALE_KEY); - localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY); + localeStorage.clearLocale(); + localeStorage.clearExtensionId(); if (Language.value() === navigator.language.toLowerCase()) { return; @@ -87,7 +145,7 @@ class WebActiveLanguagePackService implements IActiveLanguagePackService { if (language === LANGUAGE_DEFAULT) { return undefined; } - const extensionId = localStorage.getItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY); + const extensionId = localeStorage.getExtensionId(); if (extensionId) { return extensionId; } @@ -102,7 +160,7 @@ class WebActiveLanguagePackService implements IActiveLanguagePackService { // Only install extensions that are published by Microsoft and start with vscode-language-pack for extra certainty const extensionToInstall = tagResult.firstPage.find(e => e.publisher === 'MS-CEINTL' && e.name.startsWith('vscode-language-pack')); if (extensionToInstall) { - localStorage.setItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY, extensionToInstall.identifier.id); + localeStorage.setExtensionId(extensionToInstall.identifier.id); return extensionToInstall.identifier.id; } diff --git a/src/vs/workbench/services/localization/electron-sandbox/localeService.ts b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts index d7124758e38bc..9786b3c73de23 100644 --- a/src/vs/workbench/services/localization/electron-sandbox/localeService.ts +++ b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts @@ -16,7 +16,7 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { localize } from 'vs/nls'; import { toAction } from 'vs/base/common/actions'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { stripComments } from 'vs/base/common/stripComments'; +import { parse } from 'vs/base/common/jsonc'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -57,7 +57,7 @@ class NativeLocaleService implements ILocaleService { // This is the same logic that we do where argv.json is parsed so mirror that: // https://github.com/microsoft/vscode/blob/32d40cf44e893e87ac33ac4f08de1e5f7fe077fc/src/main.js#L238-L246 - JSON.parse(stripComments(content.value)); + parse(content.value); } catch (error) { this.notificationService.notify({ severity: Severity.Error, diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 1d8f6e0ebdc2e..530aa235183b0 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -12,9 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition } from 'vs/editor/common/core/position'; -import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -30,12 +28,11 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { DEFAULT_EDITOR_ASSOCIATION, IEditorPane } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; -import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/browser/keybindingsEditorInput'; -import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IKeybindingsEditorOptions, IKeybindingsEditorPane, IOpenSettingsOptions, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, USE_SPLIT_JSON_SETTING, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; +import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IKeybindingsEditorOptions, IKeybindingsEditorPane, IOpenSettingsOptions, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, ISettingsGroup, SETTINGS_AUTHORITY, USE_SPLIT_JSON_SETTING, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultRawSettingsEditorModel, DefaultSettings, DefaultSettingsEditorModel, Settings2EditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -45,6 +42,9 @@ import { isObject } from 'vs/base/common/types'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { ResourceSet } from 'vs/base/common/map'; +import { isEqual } from 'vs/base/common/resources'; +import { IURLService } from 'vs/platform/url/common/url'; const emptyEditableSettingsContent = '{\n}'; @@ -54,10 +54,21 @@ export class PreferencesService extends Disposable implements IPreferencesServic private readonly _onDispose = this._register(new Emitter()); + private readonly _onDidDefaultSettingsContentChanged = this._register(new Emitter()); + readonly onDidDefaultSettingsContentChanged = this._onDidDefaultSettingsContentChanged.event; + private _defaultUserSettingsContentModel: DefaultSettings | undefined; private _defaultWorkspaceSettingsContentModel: DefaultSettings | undefined; private _defaultFolderSettingsContentModel: DefaultSettings | undefined; + private _defaultRawSettingsEditorModel: DefaultRawSettingsEditorModel | undefined; + + private readonly _requestedDefaultSettings = new ResourceSet(); + + private _settingsGroups: ISettingsGroup[] | undefined = undefined; + private _defaultSettings: DefaultSettings | undefined = undefined; + + constructor( @IEditorService private readonly editorService: IEditorService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @@ -70,12 +81,12 @@ export class PreferencesService extends Disposable implements IPreferencesServic @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ITextModelService private readonly textModelResolverService: ITextModelService, @IKeybindingService keybindingService: IKeybindingService, - @IModelService private readonly modelService: IModelService, + @IModelService modelService: IModelService, @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, - @ILanguageService private readonly languageService: ILanguageService, @ILabelService private readonly labelService: ILabelService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, - @ITextEditorService private readonly textEditorService: ITextEditorService + @ITextEditorService private readonly textEditorService: ITextEditorService, + @IURLService urlService: IURLService ) { super(); // The default keybindings.json updates based on keyboard layouts, so here we make sure @@ -88,6 +99,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic } modelService.updateModel(model, defaultKeybindingsContents(keybindingService)); })); + + this._register(urlService.registerHandler(this)); } readonly defaultKeybindingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/keybindings.json' }); @@ -114,52 +127,39 @@ export class PreferencesService extends Disposable implements IPreferencesServic return folder ? folder.toResource(FOLDER_SETTINGS_PATH) : null; } - resolveModel(uri: URI): ITextModel | null { + hasDefaultSettingsContent(uri: URI): boolean { + return this.isDefaultSettingsResource(uri) || isEqual(uri, this.defaultSettingsRawResource) || isEqual(uri, this.defaultKeybindingsResource); + } + + getDefaultSettingsContent(uri: URI): string | undefined { if (this.isDefaultSettingsResource(uri)) { // We opened a split json editor in this case, // and this half shows the default settings. + const target = this.getConfigurationTargetFromDefaultSettingsResource(uri); - const languageSelection = this.languageService.createById('jsonc'); - const model = this._register(this.modelService.createModel('', languageSelection, uri)); - - let defaultSettings: DefaultSettings | undefined; - this.configurationService.onDidChangeConfiguration(e => { - if (e.source === ConfigurationTarget.DEFAULT) { - const model = this.modelService.getModel(uri); - if (!model) { - // model has not been given out => nothing to do - return; - } - defaultSettings = this.getDefaultSettings(target); - this.modelService.updateModel(model, defaultSettings.getContentWithoutMostCommonlyUsed(true)); - defaultSettings._onDidChange.fire(); - } - }); + const defaultSettings = this.getDefaultSettings(target); - // Check if Default settings is already created and updated in above promise - if (!defaultSettings) { - defaultSettings = this.getDefaultSettings(target); - this.modelService.updateModel(model, defaultSettings.getContentWithoutMostCommonlyUsed(true)); + if (!this._requestedDefaultSettings.has(uri)) { + this._register(defaultSettings.onDidChange(() => this._onDidDefaultSettingsContentChanged.fire(uri))); + this._requestedDefaultSettings.add(uri); } - - return model; + return defaultSettings.getContentWithoutMostCommonlyUsed(true); } - if (this.defaultSettingsRawResource.toString() === uri.toString()) { - const defaultRawSettingsEditorModel = this.instantiationService.createInstance(DefaultRawSettingsEditorModel, this.getDefaultSettings(ConfigurationTarget.USER_LOCAL)); - const languageSelection = this.languageService.createById('jsonc'); - const model = this._register(this.modelService.createModel(defaultRawSettingsEditorModel.content, languageSelection, uri)); - return model; + if (isEqual(uri, this.defaultSettingsRawResource)) { + if (!this._defaultRawSettingsEditorModel) { + this._defaultRawSettingsEditorModel = this._register(this.instantiationService.createInstance(DefaultRawSettingsEditorModel, this.getDefaultSettings(ConfigurationTarget.USER_LOCAL))); + this._register(this._defaultRawSettingsEditorModel.onDidContentChanged(() => this._onDidDefaultSettingsContentChanged.fire(uri))); + } + return this._defaultRawSettingsEditorModel.content; } - if (this.defaultKeybindingsResource.toString() === uri.toString()) { + if (isEqual(uri, this.defaultKeybindingsResource)) { const defaultKeybindingsEditorModel = this.instantiationService.createInstance(DefaultKeybindingsEditorModel, uri); - const languageSelection = this.languageService.createById('jsonc'); - const model = this._register(this.modelService.createModel(defaultKeybindingsEditorModel.content, languageSelection, uri)); - return model; + return defaultKeybindingsEditorModel.content; } - return null; + return undefined; } public async createPreferencesEditorModel(uri: URI): Promise | null> { @@ -374,7 +374,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic public createSplitJsonEditorInput(configurationTarget: ConfigurationTarget, resource: URI): EditorInput { const editableSettingsEditorInput = this.textEditorService.createTextEditor({ resource }); - const defaultPreferencesEditorInput = this.instantiationService.createInstance(TextResourceEditorInput, this.getDefaultSettingsResource(configurationTarget), undefined, undefined, undefined, undefined); + const defaultPreferencesEditorInput = this.textEditorService.createTextEditor({ resource: this.getDefaultSettingsResource(configurationTarget) }); return this.instantiationService.createInstance(SideBySideEditorInput, editableSettingsEditorInput.getName(), undefined, defaultPreferencesEditorInput, editableSettingsEditorInput); } @@ -593,6 +593,53 @@ export class PreferencesService extends Disposable implements IPreferencesServic return position; } + private get defaultSettings(): DefaultSettings { + if (!this._defaultSettings) { + this._defaultSettings = new DefaultSettings([], ConfigurationTarget.USER); + } + return this._defaultSettings; + } + + getSetting(settingId: string): ISetting | undefined { + if (!this._settingsGroups) { + this._settingsGroups = this.defaultSettings.getSettingsGroups(); + } + + for (const group of this._settingsGroups) { + for (const section of group.sections) { + for (const setting of section.settings) { + if (setting.key === settingId) { + return setting; + } + } + } + } + return undefined; + } + + /** + * Should be of the format: + * code://settings/settingName + * Examples: + * code://settings/files.autoSave + * + */ + async handleURL(uri: URI): Promise { + if (uri.authority !== SETTINGS_AUTHORITY) { + return false; + } + + const openSettingsOptions: IOpenSettingsOptions = {}; + const settingInfo = uri.path.split('/').filter(part => !!part); + if ((settingInfo.length === 0) || !this.getSetting(settingInfo[0])) { + return false; + } + + openSettingsOptions.query = settingInfo[0]; + this.openSettings(openSettingsOptions); + return true; + } + public override dispose(): void { this._onDispose.fire(); super.dispose(); diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 62f01555ef061..f6a278621e42e 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -11,9 +11,8 @@ import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope, EditPresentationTypes, IExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationDefaultValueSource, ConfigurationScope, EditPresentationTypes, IExtensionInfo } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -89,13 +88,13 @@ export interface ISetting { extensionInfo?: IExtensionInfo; validator?: (value: any) => string | null; enumItemLabels?: string[]; - allKeysAreBoolean?: boolean; editPresentation?: EditPresentationTypes; - nonLanguageSpecificDefaultValueSource?: string | IExtensionInfo; + nonLanguageSpecificDefaultValueSource?: ConfigurationDefaultValueSource; isLanguageTagSetting?: boolean; categoryLabel?: string; // Internal properties + allKeysAreBoolean?: boolean; displayExtensionId?: string; stableExtensionId?: string; prereleaseExtensionId?: string; @@ -238,12 +237,15 @@ export const IPreferencesService = createDecorator('prefere export interface IPreferencesService { readonly _serviceBrand: undefined; + readonly onDidDefaultSettingsContentChanged: Event; + userSettingsResource: URI; workspaceSettingsResource: URI | null; getFolderSettingsResource(resource: URI): URI | null; createPreferencesEditorModel(uri: URI): Promise | null>; - resolveModel(uri: URI): ITextModel | null; + getDefaultSettingsContent(uri: URI): string | undefined; + hasDefaultSettingsContent(uri: URI): boolean; createSettings2EditorModel(): Settings2EditorModel; // TODO openRawDefaultSettings(): Promise; @@ -257,6 +259,7 @@ export interface IPreferencesService { openDefaultKeybindingsFile(): Promise; openLanguageSpecificSettings(languageId: string, options?: IOpenSettingsOptions): Promise; getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise; + getSetting(settingId: string): ISetting | undefined; createSplitJsonEditorInput(configurationTarget: ConfigurationTarget, resource: URI): EditorInput; } @@ -329,3 +332,5 @@ export interface IDefineKeybindingEditorContribution extends IEditorContribution export const FOLDER_SETTINGS_PATH = '.vscode/settings.json'; export const DEFAULT_SETTINGS_EDITOR_SETTING = 'workbench.settings.openDefaultSettings'; export const USE_SPLIT_JSON_SETTING = 'workbench.settings.useSplitJSON'; + +export const SETTINGS_AUTHORITY = 'settings'; diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 507844e02c8ac..29009d0710eec 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -16,7 +16,7 @@ import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, IExtensionInfo, IRegisteredConfigurationPropertySchema, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationDefaultValueSource, ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, IRegisteredConfigurationPropertySchema, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; @@ -679,7 +679,7 @@ export class DefaultSettings extends Disposable { isLanguageTagSetting = true; } - let defaultValueSource: string | IExtensionInfo | undefined; + let defaultValueSource: ConfigurationDefaultValueSource | undefined; if (!isLanguageTagSetting) { const registeredConfigurationProp = prop as IRegisteredConfigurationPropertySchema; if (registeredConfigurationProp && registeredConfigurationProp.defaultValueSource) { @@ -1125,9 +1125,15 @@ export class DefaultRawSettingsEditorModel extends Disposable { private _content: string | null = null; + private readonly _onDidContentChanged = this._register(new Emitter()); + readonly onDidContentChanged = this._onDidContentChanged.event; + constructor(private defaultSettings: DefaultSettings) { super(); - this._register(defaultSettings.onDidChange(() => this._content = null)); + this._register(defaultSettings.onDidChange(() => { + this._content = null; + this._onDidContentChanged.fire(); + })); } get content(): string { diff --git a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts index af8f0deed6b29..a6009748222e3 100644 --- a/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/keybindingsEditorModel.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { OS, OperatingSystem } from 'vs/base/common/platform'; import { KeyCode } from 'vs/base/common/keyCodes'; diff --git a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts b/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts index 8dd960a0eb6f0..bbc9001d5d63f 100644 --- a/src/vs/workbench/services/preferences/test/browser/preferencesService.test.ts +++ b/src/vs/workbench/services/preferences/test/browser/preferencesService.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 { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IURLService } from 'vs/platform/url/common/url'; import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { TestJSONEditingService } from 'vs/workbench/services/configuration/test/common/testServices'; @@ -32,11 +33,12 @@ suite('PreferencesService', () => { testInstantiationService.stub(IJSONEditingService, TestJSONEditingService); testInstantiationService.stub(IRemoteAgentService, TestRemoteAgentService); testInstantiationService.stub(ICommandService, TestCommandService); + testInstantiationService.stub(IURLService, { registerHandler: () => { } }); // PreferencesService creates a PreferencesEditorInput which depends on IPreferencesService, add the real one, not a stub const collection = new ServiceCollection(); collection.set(IPreferencesService, new SyncDescriptor(PreferencesService)); - const instantiationService = testInstantiationService.createChild(collection); + const instantiationService = disposables.add(testInstantiationService.createChild(collection)); testObject = disposables.add(instantiationService.createInstance(PreferencesService)); }); test('options are preserved when calling openEditor', async () => { diff --git a/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts b/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts index 2b4f6f8d667be..85ef6771d8406 100644 --- a/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts +++ b/src/vs/workbench/services/preferences/test/common/preferencesValidation.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { createValidator, getInvalidTypeError } from 'vs/workbench/services/preferences/common/preferencesValidation'; diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 440aaa1e544b9..58fe33f548fd3 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -27,6 +27,7 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { stripIcons } from 'vs/base/common/iconLabels'; import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; +import { IUserActivityService } from 'vs/workbench/services/userActivity/common/userActivityService'; export class ProgressService extends Disposable implements IProgressService { @@ -40,14 +41,24 @@ export class ProgressService extends Disposable implements IProgressService { @INotificationService private readonly notificationService: INotificationService, @IStatusbarService private readonly statusbarService: IStatusbarService, @ILayoutService private readonly layoutService: ILayoutService, - @IKeybindingService private readonly keybindingService: IKeybindingService + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IUserActivityService private readonly userActivityService: IUserActivityService, ) { super(); } - async withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void): Promise { + async withProgress(options: IProgressOptions, originalTask: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void): Promise { const { location } = options; + const task = async (progress: IProgress) => { + const activeLock = this.userActivityService.markActive(); + try { + return await originalTask(progress); + } finally { + activeLock.dispose(); + } + }; + const handleStringLocation = (location: string) => { const viewContainer = this.viewDescriptorService.getViewContainerById(location); if (viewContainer) { diff --git a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts index 16f3067679c05..7bc1cec7e490b 100644 --- a/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts +++ b/src/vs/workbench/services/progress/test/browser/progressIndicator.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { AbstractProgressScope, ScopedProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; diff --git a/src/vs/workbench/services/remote/browser/remoteAgentService.ts b/src/vs/workbench/services/remote/browser/remoteAgentService.ts index ee12dd5b07060..b28ab9c941b9e 100644 --- a/src/vs/workbench/services/remote/browser/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/browser/remoteAgentService.ts @@ -27,7 +27,7 @@ export class RemoteAgentService extends AbstractRemoteAgentService implements IR @IProductService productService: IProductService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ISignService signService: ISignService, - @ILogService logService: ILogService + @ILogService logService: ILogService, ) { super(remoteSocketFactoryService, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService); } diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 9550c6a76d673..914c0a91081f9 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -3,23 +3,23 @@ * 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 { IChannel, IServerChannel, getDelayedChannel, IPCLogger } from 'vs/base/parts/ipc/common/ipc'; +import { getDelayedChannel, IChannel, IPCLogger, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Client } from 'vs/base/parts/ipc/common/ipc.net'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IDiagnosticInfo, IDiagnosticInfoOptions } from 'vs/platform/diagnostics/common/diagnostics'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; import { connectRemoteAgentManagement, IConnectionOptions, ManagementPersistentConnection, PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection'; -import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemoteAgentEnvironment, RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { RemoteExtensionEnvironmentChannelClient } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; -import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { Emitter } from 'vs/base/common/event'; +import { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; import { ISignService } from 'vs/platform/sign/common/sign'; -import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { RemoteExtensionEnvironmentChannelClient } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; +import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { IRemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; export abstract class AbstractRemoteAgentService extends Disposable implements IRemoteAgentService { @@ -35,7 +35,7 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I @IProductService productService: IProductService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ISignService signService: ISignService, - @ILogService logService: ILogService + @ILogService logService: ILogService, ) { super(); if (this._environmentService.remoteAuthority) { @@ -114,6 +114,13 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I ); } + async endConnection(): Promise { + if (this._connection) { + await this._connection.end(); + this._connection.dispose(); + } + } + private _withChannel(callback: (channel: IChannel, connection: IRemoteAgentConnection) => Promise, fallback: R): Promise { const connection = this.getConnection(); if (!connection) { @@ -159,6 +166,8 @@ class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection this._connection = null; } + end: () => Promise = () => Promise.resolve(); + getChannel(channelName: string): T { return getDelayedChannel(this._getOrCreateConnection().then(c => c.getChannel(channelName))); } @@ -222,6 +231,10 @@ class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection connection.protocol.onDidDispose(() => { connection.dispose(); }); + this.end = () => { + connection.protocol.sendDisconnect(); + return connection.protocol.drain(); + }; this._register(connection.onDidStateChange(e => this._onDidStateChange.fire(e))); return connection.client; } diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index db2d598dd8d8c..e3849b607d4a6 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -37,6 +37,11 @@ export interface IRemoteAgentService { */ getRoundTripTime(): Promise; + /** + * Gracefully ends the current connection, if any. + */ + endConnection(): Promise; + getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise; updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise; logTelemetry(eventName: string, data?: ITelemetryData): Promise; @@ -54,6 +59,7 @@ export interface IRemoteAgentConnection { readonly onReconnecting: Event; readonly onDidStateChange: Event; + end(): Promise; dispose(): void; getChannel(channelName: string): T; withChannel(channelName: string, callback: (channel: T) => Promise): Promise; diff --git a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts index 89e2791637e6d..ce9954eb03de4 100644 --- a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts +++ b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts @@ -7,7 +7,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { IRemoteExtensionsScannerService, RemoteExtensionsScannerChannelName } from 'vs/platform/remote/common/remoteExtensionsScanner'; import * as platform from 'vs/base/common/platform'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles'; @@ -16,6 +16,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IActiveLanguagePackService } from 'vs/workbench/services/localization/common/locale'; import { IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { Mutable } from 'vs/base/common/types'; class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService { @@ -44,7 +45,7 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService return await this.withChannel( async (channel) => { const profileLocation = this.userDataProfileService.currentProfile.isDefault ? undefined : (await this.remoteUserDataProfilesService.getRemoteProfile(this.userDataProfileService.currentProfile)).extensionsResource; - const scannedExtensions = await channel.call('scanExtensions', [ + const scannedExtensions = await channel.call[]>('scanExtensions', [ platform.language, profileLocation, this.extensionManagementService.getInstalledWorkspaceExtensionLocations(), @@ -64,25 +65,6 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService } } - async scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise { - try { - return await this.withChannel( - async (channel) => { - const extension = await channel.call('scanSingleExtension', [extensionLocation, isBuiltin, platform.language]); - if (extension !== null) { - extension.extensionLocation = URI.revive(extension.extensionLocation); - // ImplicitActivationEvents.updateManifest(extension); - } - return extension; - }, - null - ); - } catch (error) { - this.logService.error(error); - return null; - } - } - private withChannel(callback: (channel: IChannel) => Promise, fallback: R): Promise { const connection = this.remoteAgentService.getConnection(); if (!connection) { diff --git a/src/vs/workbench/services/remote/common/tunnelModel.ts b/src/vs/workbench/services/remote/common/tunnelModel.ts index 14f20e126ad98..bd6f0fb458972 100644 --- a/src/vs/workbench/services/remote/common/tunnelModel.ts +++ b/src/vs/workbench/services/remote/common/tunnelModel.ts @@ -459,6 +459,7 @@ export class TunnelModel extends Disposable { protocol: attributes?.get(tunnel.tunnelRemotePort)?.protocol ?? TunnelProtocol.Http, localUri: await this.makeLocalUri(tunnel.localAddress, attributes?.get(tunnel.tunnelRemotePort)), localPort: tunnel.tunnelLocalPort, + name: attributes?.get(tunnel.tunnelRemotePort)?.label, runningProcess: matchingCandidate?.detail, hasRunningProcess: !!matchingCandidate, pid: matchingCandidate?.pid, @@ -486,6 +487,7 @@ export class TunnelModel extends Disposable { protocol: attributes?.protocol ?? TunnelProtocol.Http, localUri: await this.makeLocalUri(tunnel.localAddress, attributes), localPort: tunnel.tunnelLocalPort, + name: attributes?.label, closeable: true, runningProcess: matchingCandidate?.detail, hasRunningProcess: !!matchingCandidate, diff --git a/src/vs/workbench/services/request/electron-sandbox/requestService.ts b/src/vs/workbench/services/request/electron-sandbox/requestService.ts index 0a249b6eaaadf..a0597f10ee3a2 100644 --- a/src/vs/workbench/services/request/electron-sandbox/requestService.ts +++ b/src/vs/workbench/services/request/electron-sandbox/requestService.ts @@ -7,7 +7,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ILoggerService } from 'vs/platform/log/common/log'; import { RequestService } from 'vs/platform/request/browser/requestService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IRequestService } from 'vs/platform/request/common/request'; +import { AuthInfo, Credentials, IRequestService } from 'vs/platform/request/common/request'; import { INativeHostService } from 'vs/platform/native/common/native'; export class NativeRequestService extends RequestService { @@ -24,6 +24,10 @@ export class NativeRequestService extends RequestService { return this.nativeHostService.resolveProxy(url); } + override async lookupAuthorization(authInfo: AuthInfo): Promise { + return this.nativeHostService.lookupAuthorization(authInfo); + } + override async loadCertificates(): Promise { return this.nativeHostService.loadCertificates(); } diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index cb0231edff50a..563b0dda24967 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -29,6 +29,7 @@ export const VIEW_ID = 'workbench.view.search'; export const SEARCH_RESULT_LANGUAGE_ID = 'search-result'; export const SEARCH_EXCLUDE_CONFIG = 'search.exclude'; +export const DEFAULT_MAX_SEARCH_RESULTS = 20000; // Warning: this pattern is used in the search editor to detect offsets. If you // change this, also change the search-result built-in extension @@ -461,7 +462,7 @@ export function getExcludes(configuration: ISearchConfiguration, includeSearchEx } if (!fileExcludes || !searchExcludes) { - return fileExcludes || searchExcludes; + return fileExcludes || searchExcludes || undefined; } let allExcludes: glob.IExpression = Object.create(null); diff --git a/build/azure-pipelines/common/installPlaywright.ts b/src/vs/workbench/services/search/common/searchExtTypesInternal.ts similarity index 51% rename from build/azure-pipelines/common/installPlaywright.ts rename to src/vs/workbench/services/search/common/searchExtTypesInternal.ts index 742b6e0e399c9..37ec6163bf5c1 100644 --- a/build/azure-pipelines/common/installPlaywright.ts +++ b/src/vs/workbench/services/search/common/searchExtTypesInternal.ts @@ -2,13 +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 type { FileSearchOptions, TextSearchOptions } from './searchExtTypes'; -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(); +interface RipgrepSearchOptionsCommon { + numThreads?: number; } -install(); +export interface RipgrepTextSearchOptions extends TextSearchOptions, RipgrepSearchOptionsCommon { } + +export interface RipgrepFileSearchOptions extends FileSearchOptions, RipgrepSearchOptionsCommon { } diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index cec7fb1114ce5..f4e35c525e166 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -21,7 +21,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { deserializeSearchError, FileMatch, IAITextQuery, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, isFileMatch, isProgressMessage, ITextQuery, pathIncludedInQuery, QueryType, SEARCH_RESULT_LANGUAGE_ID, SearchError, SearchErrorCode, SearchProviderType } from 'vs/workbench/services/search/common/search'; +import { DEFAULT_MAX_SEARCH_RESULTS, deserializeSearchError, FileMatch, IAITextQuery, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, isFileMatch, isProgressMessage, ITextQuery, pathIncludedInQuery, QueryType, SEARCH_RESULT_LANGUAGE_ID, SearchError, SearchErrorCode, SearchProviderType } from 'vs/workbench/services/search/common/search'; import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; export class SearchService extends Disposable implements ISearchService { @@ -529,7 +529,7 @@ export class SearchService extends Disposable implements ISearchService { } // Use editor API to find matches - const askMax = isNumber(query.maxResults) ? query.maxResults + 1 : Number.MAX_SAFE_INTEGER; + const askMax = (isNumber(query.maxResults) ? query.maxResults : DEFAULT_MAX_SEARCH_RESULTS) + 1; let matches = model.findMatches(query.contentPattern.pattern, false, !!query.contentPattern.isRegExp, !!query.contentPattern.isCaseSensitive, query.contentPattern.isWordMatch ? query.contentPattern.wordSeparators! : null, false, askMax); if (matches.length) { if (askMax && matches.length >= askMax) { diff --git a/src/vs/workbench/services/search/common/textSearchManager.ts b/src/vs/workbench/services/search/common/textSearchManager.ts index 3abb626f64b9c..d135124aa055d 100644 --- a/src/vs/workbench/services/search/common/textSearchManager.ts +++ b/src/vs/workbench/services/search/common/textSearchManager.ts @@ -11,7 +11,7 @@ import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { hasSiblingPromiseFn, IAITextQuery, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, QueryType, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; +import { DEFAULT_MAX_SEARCH_RESULTS, hasSiblingPromiseFn, IAITextQuery, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, QueryType, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; import { AITextSearchProvider, Range, TextSearchComplete, TextSearchMatch, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; export interface IFileUtils { @@ -210,7 +210,7 @@ export class TextSearchManager { followSymlinks: !fq.ignoreSymlinks, encoding: fq.fileEncoding && this.fileUtils.toCanonicalName(fq.fileEncoding), maxFileSize: this.query.maxFileSize, - maxResults: this.query.maxResults ?? Number.MAX_SAFE_INTEGER, + maxResults: this.query.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS, previewOptions: this.query.previewOptions, afterContext: this.query.afterContext, beforeContext: this.query.beforeContext diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 9b372b8dedbe6..a483dd848b0b8 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -105,7 +105,7 @@ export class FileWalker { killCmds.forEach(cmd => cmd()); } - walk(folderQueries: IFolderQuery[], extraFiles: URI[], onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, done: (error: Error | null, isLimitHit: boolean) => void): void { + walk(folderQueries: IFolderQuery[], extraFiles: URI[], numThreads: number | undefined, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, done: (error: Error | null, isLimitHit: boolean) => void): void { this.fileWalkSW = StopWatch.create(false); // Support that the file pattern is a full path to a file that exists @@ -128,7 +128,7 @@ export class FileWalker { // For each root folder this.parallel(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => { - this.call(this.cmdTraversal, this, folderQuery, onResult, onMessage, (err?: Error) => { + this.call(this.cmdTraversal, this, folderQuery, numThreads, onResult, onMessage, (err?: Error) => { if (err) { const errorMessage = toErrorMessage(err); console.error(errorMessage); @@ -181,7 +181,7 @@ export class FileWalker { } } - private cmdTraversal(folderQuery: IFolderQuery, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, cb: (err?: Error) => void): void { + private cmdTraversal(folderQuery: IFolderQuery, numThreads: number | undefined, onResult: (result: IRawFileMatch) => void, onMessage: (message: IProgressMessage) => void, cb: (err?: Error) => void): void { const rootFolder = folderQuery.folder.fsPath; const isMac = platform.isMacintosh; @@ -196,7 +196,7 @@ export class FileWalker { let leftover = ''; const tree = this.initDirectoryTree(); - const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.expression); + const ripgrep = spawnRipgrepCmd(this.config, folderQuery, this.config.includePattern, this.folderExcludePatterns.get(folderQuery.folder.fsPath)!.expression, numThreads); const cmd = ripgrep.cmd; const noSiblingsClauses = !Object.keys(ripgrep.siblingClauses).length; @@ -628,16 +628,18 @@ export class Engine implements ISearchEngine { private folderQueries: IFolderQuery[]; private extraFiles: URI[]; private walker: FileWalker; + private numThreads?: number; - constructor(config: IFileQuery) { + constructor(config: IFileQuery, numThreads?: number) { this.folderQueries = config.folderQueries; this.extraFiles = config.extraFileResources || []; + this.numThreads = numThreads; this.walker = new FileWalker(config); } search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgressMessage) => void, done: (error: Error | null, complete: ISearchEngineSuccess) => void): void { - this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error | null, isLimitHit: boolean) => { + this.walker.walk(this.folderQueries, this.extraFiles, this.numThreads, onResult, onProgress, (err: Error | null, isLimitHit: boolean) => { done(err, { limitHit: isLimitHit, stats: this.walker.getStats(), diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 8849a12897150..61bde71b572d6 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -13,7 +13,7 @@ import { basename, dirname, join, sep } from 'vs/base/common/path'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ByteSize } from 'vs/platform/files/common/files'; -import { ICachedSearchStats, IFileQuery, IFileSearchProgressItem, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileMatch, IRawFileQuery, IRawQuery, IRawSearchService, IRawTextQuery, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess, isFilePatternMatch, ITextQuery } from 'vs/workbench/services/search/common/search'; +import { DEFAULT_MAX_SEARCH_RESULTS, ICachedSearchStats, IFileQuery, IFileSearchProgressItem, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileMatch, IRawFileQuery, IRawQuery, IRawSearchService, IRawTextQuery, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess, isFilePatternMatch, ITextQuery } from 'vs/workbench/services/search/common/search'; import { Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch'; import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter'; @@ -26,7 +26,7 @@ export class SearchService implements IRawSearchService { private caches: { [cacheKey: string]: Cache } = Object.create(null); - constructor(private readonly processType: IFileSearchStats['type'] = 'searchProcess') { } + constructor(private readonly processType: IFileSearchStats['type'] = 'searchProcess', private readonly getNumThreads?: () => Promise) { } fileSearch(config: IRawFileQuery): Event { let promise: CancelablePromise; @@ -34,8 +34,9 @@ export class SearchService implements IRawSearchService { const query = reviveQuery(config); const emitter = new Emitter({ onDidAddFirstListener: () => { - promise = createCancelablePromise(token => { - return this.doFileSearchWithEngine(FileSearchEngine, query, p => emitter.fire(p), token); + promise = createCancelablePromise(async token => { + const numThreads = await this.getNumThreads?.(); + return this.doFileSearchWithEngine(FileSearchEngine, query, p => emitter.fire(p), token, SearchService.BATCH_SIZE, numThreads); }); promise.then( @@ -72,9 +73,10 @@ export class SearchService implements IRawSearchService { return emitter.event; } - private ripgrepTextSearch(config: ITextQuery, progressCallback: IProgressCallback, token: CancellationToken): Promise { + private async ripgrepTextSearch(config: ITextQuery, progressCallback: IProgressCallback, token: CancellationToken): Promise { config.maxFileSize = this.getPlatformFileLimits().maxFileSize; - const engine = new TextSearchEngineAdapter(config); + const numThreads = await this.getNumThreads?.(); + const engine = new TextSearchEngineAdapter(config, numThreads); return engine.search(token, progressCallback, progressCallback); } @@ -85,11 +87,11 @@ export class SearchService implements IRawSearchService { }; } - doFileSearch(config: IFileQuery, progressCallback: IProgressCallback, token?: CancellationToken): Promise { - return this.doFileSearchWithEngine(FileSearchEngine, config, progressCallback, token); + doFileSearch(config: IFileQuery, numThreads: number | undefined, progressCallback: IProgressCallback, token?: CancellationToken): Promise { + return this.doFileSearchWithEngine(FileSearchEngine, config, progressCallback, token, SearchService.BATCH_SIZE, numThreads); } - doFileSearchWithEngine(EngineClass: { new(config: IFileQuery): ISearchEngine }, config: IFileQuery, progressCallback: IProgressCallback, token?: CancellationToken, batchSize = SearchService.BATCH_SIZE): Promise { + doFileSearchWithEngine(EngineClass: { new(config: IFileQuery, numThreads?: number | undefined): ISearchEngine }, config: IFileQuery, progressCallback: IProgressCallback, token?: CancellationToken, batchSize = SearchService.BATCH_SIZE, threads?: number): Promise { let resultCount = 0; const fileProgressCallback: IFileProgressCallback = progress => { if (Array.isArray(progress)) { @@ -107,7 +109,7 @@ export class SearchService implements IRawSearchService { let sortedSearch = this.trySortedSearchFromCache(config, fileProgressCallback, token); if (!sortedSearch) { const walkerConfig = config.maxResults ? Object.assign({}, config, { maxResults: null }) : config; - const engine = new EngineClass(walkerConfig); + const engine = new EngineClass(walkerConfig, threads); sortedSearch = this.doSortedSearch(engine, config, progressCallback, fileProgressCallback, token); } @@ -120,7 +122,7 @@ export class SearchService implements IRawSearchService { }); } - const engine = new EngineClass(config); + const engine = new EngineClass(config, threads); return this.doSearch(engine, fileProgressCallback, batchSize, token).then(complete => { return { @@ -258,7 +260,7 @@ export class SearchService implements IRawSearchService { const query = prepareQuery(config.filePattern || ''); const compare = (matchA: IRawFileMatch, matchB: IRawFileMatch) => compareItemsByFuzzyScore(matchA, matchB, query, true, FileMatchItemAccessor, scorerCache); - const maxResults = typeof config.maxResults === 'number' ? config.maxResults : Number.MAX_VALUE; + const maxResults = typeof config.maxResults === 'number' ? config.maxResults : DEFAULT_MAX_SEARCH_RESULTS; return arrays.topAsync(results, compare, maxResults, 10000, token); } diff --git a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts index 8ad10f0ccbb5a..f833061c553a0 100644 --- a/src/vs/workbench/services/search/node/ripgrepFileSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepFileSearch.ts @@ -17,8 +17,8 @@ import { rgPath } from '@vscode/ripgrep'; // If @vscode/ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); -export function spawnRipgrepCmd(config: IFileQuery, folderQuery: IFolderQuery, includePattern?: glob.IExpression, excludePattern?: glob.IExpression) { - const rgArgs = getRgArgs(config, folderQuery, includePattern, excludePattern); +export function spawnRipgrepCmd(config: IFileQuery, folderQuery: IFolderQuery, includePattern?: glob.IExpression, excludePattern?: glob.IExpression, numThreads?: number) { + const rgArgs = getRgArgs(config, folderQuery, includePattern, excludePattern, numThreads); const cwd = folderQuery.folder.fsPath; return { cmd: cp.spawn(rgDiskPath, rgArgs.args, { cwd }), @@ -29,7 +29,7 @@ export function spawnRipgrepCmd(config: IFileQuery, folderQuery: IFolderQuery, i }; } -function getRgArgs(config: IFileQuery, folderQuery: IFolderQuery, includePattern?: glob.IExpression, excludePattern?: glob.IExpression) { +function getRgArgs(config: IFileQuery, folderQuery: IFolderQuery, includePattern?: glob.IExpression, excludePattern?: glob.IExpression, numThreads?: number) { const args = ['--files', '--hidden', '--case-sensitive', '--no-require-git']; // includePattern can't have siblingClauses @@ -71,6 +71,10 @@ function getRgArgs(config: IFileQuery, folderQuery: IFolderQuery, includePattern args.push('--quiet'); } + if (numThreads) { + args.push('--threads', `${numThreads}`); + } + args.push('--no-config'); if (folderQuery.disregardGlobalIgnoreFiles) { args.push('--no-ignore-global'); diff --git a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts index 56430c94287f4..bff10b050d6cf 100644 --- a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts +++ b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts @@ -6,27 +6,33 @@ import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { OutputChannel } from 'vs/workbench/services/search/node/ripgrepSearchUtils'; import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; -import { TextSearchProvider, TextSearchComplete, TextSearchResult, TextSearchQuery, TextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes'; +import { TextSearchProvider, TextSearchComplete, TextSearchResult, TextSearchQuery, TextSearchOptions, } from 'vs/workbench/services/search/common/searchExtTypes'; import { Progress } from 'vs/platform/progress/common/progress'; import { Schemas } from 'vs/base/common/network'; +import type { RipgrepTextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypesInternal'; export class RipgrepSearchProvider implements TextSearchProvider { private inProgress: Set = new Set(); - constructor(private outputChannel: OutputChannel) { + constructor(private outputChannel: OutputChannel, private getNumThreads: () => Promise) { process.once('exit', () => this.dispose()); } - provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { - const engine = new RipgrepTextSearchEngine(this.outputChannel); + async provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { + const numThreads = await this.getNumThreads(); + const engine = new RipgrepTextSearchEngine(this.outputChannel, numThreads); + const extendedOptions: RipgrepTextSearchOptions = { + ...options, + numThreads, + }; if (options.folder.scheme === Schemas.vscodeUserData) { // Ripgrep search engine can only provide file-scheme results, but we want to use it to search some schemes that are backed by the filesystem, but with some other provider as the frontend, // case in point vscode-userdata. In these cases we translate the query to a file, and translate the results back to the frontend scheme. - const translatedOptions = { ...options, folder: options.folder.with({ scheme: Schemas.file }) }; + const translatedOptions = { ...extendedOptions, folder: options.folder.with({ scheme: Schemas.file }) }; const progressTranslator = new Progress(data => progress.report({ ...data, uri: data.uri.with({ scheme: options.folder.scheme }) })); return this.withToken(token, token => engine.provideTextSearchResults(query, translatedOptions, progressTranslator, token)); } else { - return this.withToken(token, token => engine.provideTextSearchResults(query, options, progress, token)); + return this.withToken(token, token => engine.provideTextSearchResults(query, extendedOptions, progress, token)); } } diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 727a1ba64c7c3..4740a66111967 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -18,13 +18,14 @@ import { Range, TextSearchComplete, TextSearchContext, TextSearchMatch, TextSear import { AST as ReAST, RegExpParser, RegExpVisitor } from 'vscode-regexpp'; import { rgPath } from '@vscode/ripgrep'; import { anchorGlob, createTextSearchResult, IOutputChannel, Maybe } from './ripgrepSearchUtils'; +import type { RipgrepTextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypesInternal'; // If @vscode/ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); export class RipgrepTextSearchEngine { - constructor(private outputChannel: IOutputChannel) { } + constructor(private outputChannel: IOutputChannel, private readonly _numThreads?: number | undefined) { } provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { this.outputChannel.appendLine(`provideTextSearchResults ${query.pattern}, ${JSON.stringify({ @@ -37,7 +38,11 @@ export class RipgrepTextSearchEngine { return new Promise((resolve, reject) => { token.onCancellationRequested(() => cancel()); - const rgArgs = getRgArgs(query, options); + const extendedOptions: RipgrepTextSearchOptions = { + ...options, + numThreads: this._numThreads + }; + const rgArgs = getRgArgs(query, extendedOptions); const cwd = options.folder.fsPath; @@ -368,7 +373,7 @@ function getNumLinesAndLastNewlineLength(text: string): { numLines: number; last } // exported for testing -export function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): string[] { +export function getRgArgs(query: TextSearchQuery, options: RipgrepTextSearchOptions): string[] { const args = ['--hidden', '--no-require-git']; args.push(query.isCaseSensitive ? '--case-sensitive' : '--ignore-case'); @@ -422,6 +427,10 @@ export function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): s args.push('--encoding', options.encoding); } + if (options.numThreads) { + args.push('--threads', `${options.numThreads}`); + } + // Ripgrep handles -- as a -- arg separator. Only --. // - is ok, --- is ok, --some-flag is also ok. Need to special case. if (query.pattern === '--') { diff --git a/src/vs/workbench/services/search/node/textSearchAdapter.ts b/src/vs/workbench/services/search/node/textSearchAdapter.ts index 45b8aab292255..58145f0af5639 100644 --- a/src/vs/workbench/services/search/node/textSearchAdapter.ts +++ b/src/vs/workbench/services/search/node/textSearchAdapter.ts @@ -11,7 +11,7 @@ import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textS export class TextSearchEngineAdapter { - constructor(private query: ITextQuery) { } + constructor(private query: ITextQuery, private numThreads?: number) { } search(token: CancellationToken, onResult: (matches: ISerializedFileMatch[]) => void, onMessage: (message: IProgressMessage) => void): Promise { if ((!this.query.folderQueries || !this.query.folderQueries.length) && (!this.query.extraFileResources || !this.query.extraFileResources.length)) { @@ -30,7 +30,7 @@ export class TextSearchEngineAdapter { onMessage({ message: msg }); } }; - const textSearchManager = new NativeTextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), pfs); + const textSearchManager = new NativeTextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel, this.numThreads), pfs); return new Promise((resolve, reject) => { return textSearchManager .search( diff --git a/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts b/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts index 53209bfbe45e7..e117233667371 100644 --- a/src/vs/workbench/services/search/test/browser/queryBuilder.test.ts +++ b/src/vs/workbench/services/search/test/browser/queryBuilder.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 { IExpression } from 'vs/base/common/glob'; import { join } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/services/search/test/common/ignoreFile.test.ts b/src/vs/workbench/services/search/test/common/ignoreFile.test.ts index d5627df9bf157..75a456be6ec85 100644 --- a/src/vs/workbench/services/search/test/common/ignoreFile.test.ts +++ b/src/vs/workbench/services/search/test/common/ignoreFile.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { IgnoreFile } from 'vs/workbench/services/search/common/ignoreFile'; diff --git a/src/vs/workbench/services/search/test/common/queryBuilder.test.ts b/src/vs/workbench/services/search/test/common/queryBuilder.test.ts index a10084b5db914..a4a7ce1cf1855 100644 --- a/src/vs/workbench/services/search/test/common/queryBuilder.test.ts +++ b/src/vs/workbench/services/search/test/common/queryBuilder.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 } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/services/search/test/common/replace.test.ts b/src/vs/workbench/services/search/test/common/replace.test.ts index 5b4b721a30f4b..871c7e39f6fb5 100644 --- a/src/vs/workbench/services/search/test/common/replace.test.ts +++ b/src/vs/workbench/services/search/test/common/replace.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 { ReplacePattern } from 'vs/workbench/services/search/common/replace'; diff --git a/src/vs/workbench/services/search/test/common/search.test.ts b/src/vs/workbench/services/search/test/common/search.test.ts index 13fdd72544172..5f0ec330f8ab4 100644 --- a/src/vs/workbench/services/search/test/common/search.test.ts +++ b/src/vs/workbench/services/search/test/common/search.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 { ITextSearchPreviewOptions, OneLineRange, TextSearchMatch, SearchRange } from 'vs/workbench/services/search/common/search'; diff --git a/src/vs/workbench/services/search/test/common/searchHelpers.test.ts b/src/vs/workbench/services/search/test/common/searchHelpers.test.ts index bc51969faef04..2d9db98c70d2a 100644 --- a/src/vs/workbench/services/search/test/common/searchHelpers.test.ts +++ b/src/vs/workbench/services/search/test/common/searchHelpers.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { FindMatch, ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts b/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts index ac3dd097c7637..3cfd50a70f539 100644 --- a/src/vs/workbench/services/search/test/node/fileSearch.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/fileSearch.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 { FileAccess } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; @@ -25,11 +25,13 @@ const MULTIROOT_QUERIES: IFolderQuery[] = [ { folder: URI.file(MORE_FIXTURES) } ]; +const numThreads = undefined; + async function doSearchTest(query: IFileQuery, expectedResultCount: number | Function): Promise { const svc = new SearchService(); const results: ISerializedSearchProgressItem[] = []; - await svc.doFileSearch(query, e => { + await svc.doFileSearch(query, numThreads, e => { if (!isProgressMessage(e)) { if (Array.isArray(e)) { results.push(...e); diff --git a/src/vs/workbench/services/search/test/node/rawSearchService.integrationTest.ts b/src/vs/workbench/services/search/test/node/rawSearchService.integrationTest.ts index 1838e82c12d8e..7c2a5407a3780 100644 --- a/src/vs/workbench/services/search/test/node/rawSearchService.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/rawSearchService.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 { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts b/src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts index fae2e5b6c4c17..98d8e8a421d1b 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { fixDriveC, getAbsoluteGlob } from 'vs/workbench/services/search/node/ripgrepFileSearch'; diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts index 09f3320cf2811..e93c4efb2691e 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { fixRegexNewline, IRgMatch, IRgMessage, RipgrepParser, unicodeEscapesToPCRE2, fixNewline, getRgArgs, performBraceExpansionForRipgrep } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; diff --git a/src/vs/workbench/services/search/test/node/search.integrationTest.ts b/src/vs/workbench/services/search/test/node/search.integrationTest.ts index cc9b541e70443..ae3a4b4fe7445 100644 --- a/src/vs/workbench/services/search/test/node/search.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/search.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 path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; diff --git a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts index 206de297464a9..503b314ffbe53 100644 --- a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/textSearch.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 path from 'vs/base/common/path'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as glob from 'vs/base/common/glob'; diff --git a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts index 693c4e9f0c024..1eb64535a8d47 100644 --- a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts +++ b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/services/statusbar/browser/statusbar.ts b/src/vs/workbench/services/statusbar/browser/statusbar.ts index 1d00e77bca787..73fac7bb9248c 100644 --- a/src/vs/workbench/services/statusbar/browser/statusbar.ts +++ b/src/vs/workbench/services/statusbar/browser/statusbar.ts @@ -174,9 +174,9 @@ export interface IStatusbarEntry { /** * Will enable a spinning icon in front of the text to indicate progress. When `true` is - * specified, `syncing` will be used. + * specified, `loading` will be used. */ - readonly showProgress?: boolean | 'syncing' | 'loading'; + readonly showProgress?: boolean | 'loading' | 'syncing'; /** * The kind of status bar entry. This applies different colors to the entry. diff --git a/src/vs/workbench/services/suggest/browser/media/suggest.css b/src/vs/workbench/services/suggest/browser/media/suggest.css index cf39f56b81602..b540839efa3cc 100644 --- a/src/vs/workbench/services/suggest/browser/media/suggest.css +++ b/src/vs/workbench/services/suggest/browser/media/suggest.css @@ -8,9 +8,8 @@ * layer breakers and shipping with the standalone monaco editor. */ -/* TODO: Position correctly */ .workbench-suggest-widget { - position: absolute; + position: fixed; left: 0; top: 0; } diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts index 3aa43e4c72711..fd3b2131cf5f0 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionItem.ts @@ -38,6 +38,6 @@ export class SimpleCompletionItem { readonly completion: ISimpleCompletion ) { // ensure lower-variants (perf) - this.labelLow = this.completion.label.toLowerCase(); + this.labelLow = (this.completion.completionText ?? this.completion.label).toLowerCase(); } } diff --git a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts index 1418e462f0088..628ee0a809e14 100644 --- a/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts +++ b/src/vs/workbench/services/suggest/browser/simpleCompletionModel.ts @@ -165,7 +165,7 @@ export class SimpleCompletionModel { } else { // by default match `word` against the `label` - const match = scoreFn(word, wordLow, wordPos, item.completion.label, item.labelLow, 0, this._fuzzyScoreOptions); + const match = scoreFn(word, wordLow, wordPos, item.completion.completionText ?? item.completion.label, item.labelLow, 0, this._fuzzyScoreOptions); if (!match) { continue; // NO match } @@ -177,7 +177,7 @@ export class SimpleCompletionModel { target.push(item); // update stats - labelLengths.push(item.completion.label.length); + labelLengths.push((item.completion.completionText ?? item.completion.label).length); } this._filteredItems = target.sort((a, b) => b.score[0] - a.score[0]); diff --git a/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts index 950605ae82595..c11cf65851478 100644 --- a/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/browser/commonProperties.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 { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/browser/workbenchCommonProperties'; import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts index e51736b4ee9a8..bb4004c54197b 100644 --- a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { release, hostname } from 'os'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/common/workbenchCommonProperties'; import { StorageScope, InMemoryStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts index bc98f7239ce53..112d76ec0d930 100644 --- a/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerTokenizer.ts @@ -19,6 +19,7 @@ import { TokenizationSupportWithLineLimit } from 'vs/workbench/services/textMate import type { StackDiff, StateStack, diffStateStacksRefEq } from 'vscode-textmate'; import { ICreateGrammarResult } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; import { StateDeltas } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; +import { Disposable } from 'vs/base/common/lifecycle'; export interface TextMateModelTokenizerHost { getOrCreateGrammar(languageId: string, encodedLanguageId: LanguageId): Promise; @@ -98,6 +99,7 @@ export class TextMateWorkerTokenizer extends MirrorTextModel { }, false ), + Disposable.None, this._maxTokenizationLineLength ); this._tokenizerWithStateStore = new TokenizerWithStateStore(this._lines.length, tokenizationSupport); diff --git a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts index 74a52fae9ff3f..02eef664f6f65 100644 --- a/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts +++ b/src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts @@ -85,9 +85,9 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate this._updateTheme(this._themeService.getColorTheme(), false); })); - this._languageService.onDidRequestRichLanguageFeatures((languageId) => { + this._register(this._languageService.onDidRequestRichLanguageFeatures((languageId) => { this._createdModes.push(languageId); - }); + })); } private getAsyncTokenizationEnabled(): boolean { @@ -171,7 +171,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate } } - const validLanguageId = grammar.language && this._languageService.isRegisteredLanguageId(grammar.language) ? grammar.language : null; + const validLanguageId = grammar.language && this._languageService.isRegisteredLanguageId(grammar.language) ? grammar.language : undefined; function asStringArray(array: unknown, defaultValue: string[]): string[] { if (!Array.isArray(array)) { @@ -185,7 +185,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate return { location: grammarLocation, - language: validLanguageId || undefined, + language: validLanguageId, scopeName: grammar.scopeName, embeddedLanguages: embeddedLanguages, tokenTypes: tokenTypes, @@ -302,14 +302,14 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate }, true, ); - tokenization.onDidEncounterLanguage((encodedLanguageId) => { + const disposable = tokenization.onDidEncounterLanguage((encodedLanguageId) => { if (!this._encounteredLanguages[encodedLanguageId]) { const languageId = this._languageService.languageIdCodec.decodeLanguageId(encodedLanguageId); this._encounteredLanguages[encodedLanguageId] = true; this._languageService.requestBasicLanguageFeatures(languageId); } }); - return new TokenizationSupportWithLineLimit(encodedLanguageId, tokenization, maxTokenizationLineLength); + return new TokenizationSupportWithLineLimit(encodedLanguageId, tokenization, disposable, maxTokenizationLineLength); } catch (err) { if (err.message && err.message === missingTMGrammarErrorMessage) { // Don't log this error message diff --git a/src/vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit.ts b/src/vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit.ts index f5bd00e965ee8..a32113c6e8e27 100644 --- a/src/vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit.ts +++ b/src/vs/workbench/services/textMate/browser/tokenizationSupport/tokenizationSupportWithLineLimit.ts @@ -7,7 +7,7 @@ import { LanguageId } from 'vs/editor/common/encodedTokenAttributes'; import { EncodedTokenizationResult, IBackgroundTokenizationStore, IBackgroundTokenizer, IState, ITokenizationSupport, TokenizationResult } from 'vs/editor/common/languages'; import { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize'; import { ITextModel } from 'vs/editor/common/model'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IObservable, keepObserved } from 'vs/base/common/observable'; export class TokenizationSupportWithLineLimit extends Disposable implements ITokenizationSupport { @@ -18,11 +18,13 @@ export class TokenizationSupportWithLineLimit extends Disposable implements ITok constructor( private readonly _encodedLanguageId: LanguageId, private readonly _actual: ITokenizationSupport, + disposable: IDisposable, private readonly _maxTokenizationLineLength: IObservable, ) { super(); this._register(keepObserved(this._maxTokenizationLineLength)); + this._register(disposable); } getInitialState(): IState { diff --git a/src/vs/workbench/services/textMate/common/TMGrammars.ts b/src/vs/workbench/services/textMate/common/TMGrammars.ts index b460653c97b20..150a91d9120b2 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammars.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammars.ts @@ -16,7 +16,7 @@ export interface TokenTypesContribution { } export interface ITMSyntaxExtensionPoint { - language: string; + language?: string; // undefined if the grammar is only included by other grammars scopeName: string; path: string; embeddedLanguages: IEmbeddedLanguagesMap; diff --git a/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts b/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts index 917849e3b576f..3844a0f51d147 100644 --- a/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts +++ b/src/vs/workbench/services/textMate/test/browser/arrayOperation.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from 'vs/workbench/services/textMate/browser/arrayOperation'; diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index b388e7cc33477..859fa8a1c1af1 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -304,7 +304,12 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // read through encoding library return toDecodeStream(stream, { acceptTextOnly: options?.acceptTextOnly ?? false, - guessEncoding: options?.autoGuessEncoding || this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'), + guessEncoding: + options?.autoGuessEncoding || + this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'), + candidateGuessEncodings: + options?.candidateGuessEncodings || + this.textResourceConfigurationService.getValue(resource, 'files.candidateGuessEncodings'), overwriteEncoding: async detectedEncoding => { const { encoding } = await this.encoding.getPreferredReadEncoding(resource, options, detectedEncoding ?? undefined); diff --git a/src/vs/workbench/services/textfile/common/encoding.ts b/src/vs/workbench/services/textfile/common/encoding.ts index 65e8117bb5a61..dbee934f6c3d4 100644 --- a/src/vs/workbench/services/textfile/common/encoding.ts +++ b/src/vs/workbench/services/textfile/common/encoding.ts @@ -7,6 +7,7 @@ import { Readable, ReadableStream, newWriteableStream, listenStream } from 'vs/b import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { importAMDNodeModule } from 'vs/amdX'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { coalesce } from 'vs/base/common/arrays'; export const UTF8 = 'utf8'; export const UTF8_with_bom = 'utf8bom'; @@ -31,6 +32,7 @@ const AUTO_ENCODING_GUESS_MAX_BYTES = 512 * 128; // set an upper limit for the export interface IDecodeStreamOptions { acceptTextOnly: boolean; guessEncoding: boolean; + candidateGuessEncodings: string[]; minBytesRequiredForDetection?: number; overwriteEncoding(detectedEncoding: string | null): Promise; @@ -134,7 +136,7 @@ export function toDecodeStream(source: VSBufferReadableStream, options: IDecodeS const detected = await detectEncodingFromBuffer({ buffer: VSBuffer.concat(bufferedChunks), bytesRead: bytesBuffered - }, options.guessEncoding); + }, options.guessEncoding, options.candidateGuessEncodings); // throw early if the source seems binary and // we are instructed to only accept text @@ -317,7 +319,7 @@ const IGNORE_ENCODINGS = ['ascii', 'utf-16', 'utf-32']; /** * Guesses the encoding from buffer. */ -async function guessEncodingByBuffer(buffer: VSBuffer): Promise { +async function guessEncodingByBuffer(buffer: VSBuffer, candidateGuessEncodings?: string[]): Promise { const jschardet = await importAMDNodeModule('jschardet', 'dist/jschardet.min.js'); // ensure to limit buffer for guessing due to https://github.com/aadsm/jschardet/issues/53 @@ -328,7 +330,15 @@ async function guessEncodingByBuffer(buffer: VSBuffer): Promise { // https://github.com/aadsm/jschardet/blob/v2.1.1/src/index.js#L36-L40 const binaryString = encodeLatin1(limitedBuffer.buffer); - const guessed = jschardet.detect(binaryString); + // ensure to convert candidate encodings to jschardet encoding names if provided + if (candidateGuessEncodings) { + candidateGuessEncodings = coalesce(candidateGuessEncodings.map(e => toJschardetEncoding(e))); + if (candidateGuessEncodings.length === 0) { + candidateGuessEncodings = undefined; + } + } + + const guessed = jschardet.detect(binaryString, candidateGuessEncodings ? { detectEncodings: candidateGuessEncodings } : undefined); if (!guessed || !guessed.encoding) { return null; } @@ -351,13 +361,24 @@ const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { 'big5': 'cp950' }; +function normalizeEncoding(encodingName: string): string { + return encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); +} + function toIconvLiteEncoding(encodingName: string): string { - const normalizedEncodingName = encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); + const normalizedEncodingName = normalizeEncoding(encodingName); const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName]; return mapped || normalizedEncodingName; } +function toJschardetEncoding(encodingName: string): string | undefined { + const normalizedEncodingName = normalizeEncoding(encodingName); + const mapped = GUESSABLE_ENCODINGS[normalizedEncodingName]; + + return mapped.guessableName; +} + function encodeLatin1(buffer: Uint8Array): string { let result = ''; for (let i = 0; i < buffer.length; i++) { @@ -415,9 +436,9 @@ export interface IReadResult { bytesRead: number; } -export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: false): IDetectedEncodingResult; -export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: boolean): Promise; -export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, autoGuessEncoding?: boolean): Promise | IDetectedEncodingResult { +export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: false, candidateGuessEncodings?: string[]): IDetectedEncodingResult; +export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: boolean, candidateGuessEncodings?: string[]): Promise; +export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, autoGuessEncoding?: boolean, candidateGuessEncodings?: string[]): Promise | IDetectedEncodingResult { // Always first check for BOM to find out about encoding let encoding = detectEncodingByBOMFromBuffer(buffer, bytesRead); @@ -474,7 +495,7 @@ export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, aut // Auto guess encoding if configured if (autoGuessEncoding && !seemsBinary && !encoding && buffer) { - return guessEncodingByBuffer(buffer.slice(0, bytesRead)).then(guessedEncoding => { + return guessEncodingByBuffer(buffer.slice(0, bytesRead), candidateGuessEncodings).then(guessedEncoding => { return { seemsBinary: false, encoding: guessedEncoding @@ -485,12 +506,15 @@ export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, aut return { seemsBinary, encoding }; } -export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; labelShort: string; order: number; encodeOnly?: boolean; alias?: string } } = { +type EncodingsMap = { [encoding: string]: { labelLong: string; labelShort: string; order: number; encodeOnly?: boolean; alias?: string; guessableName?: string } }; + +export const SUPPORTED_ENCODINGS: EncodingsMap = { utf8: { labelLong: 'UTF-8', labelShort: 'UTF-8', order: 1, - alias: 'utf8bom' + alias: 'utf8bom', + guessableName: 'UTF-8' }, utf8bom: { labelLong: 'UTF-8 with BOM', @@ -502,17 +526,20 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab utf16le: { labelLong: 'UTF-16 LE', labelShort: 'UTF-16 LE', - order: 3 + order: 3, + guessableName: 'UTF-16LE' }, utf16be: { labelLong: 'UTF-16 BE', labelShort: 'UTF-16 BE', - order: 4 + order: 4, + guessableName: 'UTF-16BE' }, windows1252: { labelLong: 'Western (Windows 1252)', labelShort: 'Windows 1252', - order: 5 + order: 5, + guessableName: 'windows-1252' }, iso88591: { labelLong: 'Western (ISO 8859-1)', @@ -567,12 +594,14 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab windows1250: { labelLong: 'Central European (Windows 1250)', labelShort: 'Windows 1250', - order: 16 + order: 16, + guessableName: 'windows-1250' }, iso88592: { labelLong: 'Central European (ISO 8859-2)', labelShort: 'ISO 8859-2', - order: 17 + order: 17, + guessableName: 'ISO-8859-2' }, cp852: { labelLong: 'Central European (CP 852)', @@ -582,22 +611,26 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab windows1251: { labelLong: 'Cyrillic (Windows 1251)', labelShort: 'Windows 1251', - order: 19 + order: 19, + guessableName: 'windows-1251' }, cp866: { labelLong: 'Cyrillic (CP 866)', labelShort: 'CP 866', - order: 20 + order: 20, + guessableName: 'IBM866' }, iso88595: { labelLong: 'Cyrillic (ISO 8859-5)', labelShort: 'ISO 8859-5', - order: 21 + order: 21, + guessableName: 'ISO-8859-5' }, koi8r: { labelLong: 'Cyrillic (KOI8-R)', labelShort: 'KOI8-R', - order: 22 + order: 22, + guessableName: 'KOI8-R' }, koi8u: { labelLong: 'Cyrillic (KOI8-U)', @@ -612,22 +645,26 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab windows1253: { labelLong: 'Greek (Windows 1253)', labelShort: 'Windows 1253', - order: 25 + order: 25, + guessableName: 'windows-1253' }, iso88597: { labelLong: 'Greek (ISO 8859-7)', labelShort: 'ISO 8859-7', - order: 26 + order: 26, + guessableName: 'ISO-8859-7' }, windows1255: { labelLong: 'Hebrew (Windows 1255)', labelShort: 'Windows 1255', - order: 27 + order: 27, + guessableName: 'windows-1255' }, iso88598: { labelLong: 'Hebrew (ISO 8859-8)', labelShort: 'ISO 8859-8', - order: 28 + order: 28, + guessableName: 'ISO-8859-8' }, iso885910: { labelLong: 'Nordic (ISO 8859-10)', @@ -667,7 +704,8 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab cp950: { labelLong: 'Traditional Chinese (Big5)', labelShort: 'Big5', - order: 36 + order: 36, + guessableName: 'Big5' }, big5hkscs: { labelLong: 'Traditional Chinese (Big5-HKSCS)', @@ -677,17 +715,20 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab shiftjis: { labelLong: 'Japanese (Shift JIS)', labelShort: 'Shift JIS', - order: 38 + order: 38, + guessableName: 'SHIFT_JIS' }, eucjp: { labelLong: 'Japanese (EUC-JP)', labelShort: 'EUC-JP', - order: 39 + order: 39, + guessableName: 'EUC-JP' }, euckr: { labelLong: 'Korean (EUC-KR)', labelShort: 'EUC-KR', - order: 40 + order: 40, + guessableName: 'EUC-KR' }, windows874: { labelLong: 'Thai (Windows 874)', @@ -712,7 +753,8 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab gb2312: { labelLong: 'Simplified Chinese (GB 2312)', labelShort: 'GB 2312', - order: 45 + order: 45, + guessableName: 'GB2312' }, cp865: { labelLong: 'Nordic DOS (CP 865)', @@ -725,3 +767,14 @@ export const SUPPORTED_ENCODINGS: { [encoding: string]: { labelLong: string; lab order: 47 } }; + +export const GUESSABLE_ENCODINGS: EncodingsMap = (() => { + const guessableEncodings: EncodingsMap = {}; + for (const encoding in SUPPORTED_ENCODINGS) { + if (SUPPORTED_ENCODINGS[encoding].guessableName) { + guessableEncodings[encoding] = SUPPORTED_ENCODINGS[encoding]; + } + } + + return guessableEncodings; +})(); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 3eeac7d1ed5fa..3150fdd7b1d57 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -824,8 +824,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil title: localize('saveParticipants', "Saving '{0}'", this.name), location: ProgressLocation.Window, cancellable: true, - delay: this.isDirty() ? 3000 : 5000, - type: 'loading' + delay: this.isDirty() ? 3000 : 5000 }, progress => { return this.doSaveSequential(versionId, options, progress, saveCancellation); }, () => { diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 73abb110fb136..d33c93d5d545d 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -131,6 +131,11 @@ export interface IReadTextFileEncodingOptions { */ readonly autoGuessEncoding?: boolean; + /** + * The optional candidateGuessEncodings parameter limits the allowed encodings to guess from. + */ + readonly candidateGuessEncodings?: string[]; + /** * The optional acceptTextOnly parameter allows to fail this request early if the file * contents are not textual. diff --git a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts index 749f3cc6dc57d..af8317daf52da 100644 --- a/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textEditorService.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { IResourceDiffEditorInput, IResourceSideBySideEditorInput, isResourceDiffEditorInput, isResourceSideBySideEditorInput, isUntitledResourceEditorInput } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, registerTestResourceEditor, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts index 866265c14fa5a..622f4e90151f7 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.integrationTest.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 069b90a09d92c..e7d5625101883 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { EncodingMode, TextFileEditorModelState, snapshotToString, isTextFileEditorModel, ITextFileEditorModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles'; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts index 6cbeccc887666..8c3c0574bf64b 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, ITestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index e3f16f160656b..a1cc6f8bafc44 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -3,7 +3,7 @@ * Licensed 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, TestServiceAccessor, ITestTextFileEditorModelManager } from 'vs/workbench/test/browser/workbenchTestServices'; import { ensureNoDisposablesAreLeakedInTestSuite, toResource } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/services/textfile/test/common/fixtures/files.ts b/src/vs/workbench/services/textfile/test/common/fixtures/files.ts index f3d0c5d6cd21d..3167d521985cc 100644 --- a/src/vs/workbench/services/textfile/test/common/fixtures/files.ts +++ b/src/vs/workbench/services/textfile/test/common/fixtures/files.ts @@ -68,6 +68,9 @@ fixtures['index.html'] = Uint8Array.from([ 60, 33, 68, 79, 67, 84, 89, 80, 69, 32, 104, 116, 109, 108, 62, 10, 60, 104, 116, 109, 108, 62, 10, 60, 104, 101, 97, 100, 32, 105, 100, 61, 39, 104, 101, 97, 100, 73, 68, 39, 62, 10, 32, 32, 32, 32, 60, 109, 101, 116, 97, 32, 104, 116, 116, 112, 45, 101, 113, 117, 105, 118, 61, 34, 88, 45, 85, 65, 45, 67, 111, 109, 112, 97, 116, 105, 98, 108, 101, 34, 32, 99, 111, 110, 116, 101, 110, 116, 61, 34, 73, 69, 61, 101, 100, 103, 101, 34, 32, 47, 62, 10, 32, 32, 32, 32, 60, 116, 105, 116, 108, 101, 62, 83, 116, 114, 97, 100, 97, 32, 60, 47, 116, 105, 116, 108, 101, 62, 10, 32, 32, 32, 32, 60, 108, 105, 110, 107, 32, 104, 114, 101, 102, 61, 34, 115, 105, 116, 101, 46, 99, 115, 115, 34, 32, 114, 101, 108, 61, 34, 115, 116, 121, 108, 101, 115, 104, 101, 101, 116, 34, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 99, 115, 115, 34, 32, 47, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 115, 114, 99, 61, 34, 106, 113, 117, 101, 114, 121, 45, 49, 46, 52, 46, 49, 46, 106, 115, 34, 62, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 115, 114, 99, 61, 34, 46, 46, 47, 99, 111, 109, 112, 105, 108, 101, 114, 47, 100, 116, 114, 101, 101, 46, 106, 115, 34, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 34, 62, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 115, 114, 99, 61, 34, 46, 46, 47, 99, 111, 109, 112, 105, 108, 101, 114, 47, 116, 121, 112, 101, 115, 99, 114, 105, 112, 116, 46, 106, 115, 34, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 34, 62, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 34, 62, 10, 10, 32, 32, 32, 32, 47, 47, 32, 67, 111, 109, 112, 105, 108, 101, 32, 115, 116, 114, 97, 100, 97, 32, 115, 111, 117, 114, 99, 101, 32, 105, 110, 116, 111, 32, 114, 101, 115, 117, 108, 116, 105, 110, 103, 32, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 10, 32, 32, 32, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 99, 111, 109, 112, 105, 108, 101, 40, 112, 114, 111, 103, 44, 32, 108, 105, 98, 84, 101, 120, 116, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 111, 117, 116, 102, 105, 108, 101, 32, 61, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 115, 111, 117, 114, 99, 101, 58, 32, 34, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 87, 114, 105, 116, 101, 58, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 40, 115, 41, 32, 123, 32, 116, 104, 105, 115, 46, 115, 111, 117, 114, 99, 101, 32, 43, 61, 32, 115, 59, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 87, 114, 105, 116, 101, 76, 105, 110, 101, 58, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 40, 115, 41, 32, 123, 32, 116, 104, 105, 115, 46, 115, 111, 117, 114, 99, 101, 32, 43, 61, 32, 115, 32, 43, 32, 34, 92, 114, 34, 59, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 32, 61, 32, 91, 93, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 99, 111, 109, 112, 105, 108, 101, 114, 61, 110, 101, 119, 32, 84, 111, 111, 108, 115, 46, 84, 121, 112, 101, 83, 99, 114, 105, 112, 116, 67, 111, 109, 112, 105, 108, 101, 114, 40, 111, 117, 116, 102, 105, 108, 101, 44, 116, 114, 117, 101, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 115, 101, 116, 69, 114, 114, 111, 114, 67, 97, 108, 108, 98, 97, 99, 107, 40, 102, 117, 110, 99, 116, 105, 111, 110, 40, 115, 116, 97, 114, 116, 44, 108, 101, 110, 44, 32, 109, 101, 115, 115, 97, 103, 101, 41, 32, 123, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 46, 112, 117, 115, 104, 40, 123, 115, 116, 97, 114, 116, 58, 115, 116, 97, 114, 116, 44, 32, 108, 101, 110, 58, 108, 101, 110, 44, 32, 109, 101, 115, 115, 97, 103, 101, 58, 109, 101, 115, 115, 97, 103, 101, 125, 41, 59, 32, 125, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 97, 100, 100, 85, 110, 105, 116, 40, 108, 105, 98, 84, 101, 120, 116, 44, 34, 108, 105, 98, 46, 116, 115, 34, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 97, 100, 100, 85, 110, 105, 116, 40, 112, 114, 111, 103, 44, 34, 105, 110, 112, 117, 116, 46, 116, 115, 34, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 116, 121, 112, 101, 67, 104, 101, 99, 107, 40, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 114, 46, 101, 109, 105, 116, 40, 41, 59, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 105, 102, 40, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 46, 108, 101, 110, 103, 116, 104, 32, 62, 32, 48, 32, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 47, 47, 116, 104, 114, 111, 119, 32, 110, 101, 119, 32, 69, 114, 114, 111, 114, 40, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 9, 119, 104, 105, 108, 101, 40, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 91, 48, 93, 32, 61, 61, 32, 39, 47, 39, 32, 38, 38, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 91, 49, 93, 32, 61, 61, 32, 39, 47, 39, 32, 38, 38, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 91, 50, 93, 32, 61, 61, 32, 39, 32, 39, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 32, 61, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 46, 115, 108, 105, 99, 101, 40, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 46, 105, 110, 100, 101, 120, 79, 102, 40, 39, 92, 114, 39, 41, 43, 49, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 101, 114, 114, 111, 114, 80, 114, 101, 102, 105, 120, 32, 61, 32, 34, 34, 59, 10, 9, 102, 111, 114, 40, 118, 97, 114, 32, 105, 32, 61, 32, 48, 59, 105, 60, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 46, 108, 101, 110, 103, 116, 104, 59, 105, 43, 43, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 101, 114, 114, 111, 114, 80, 114, 101, 102, 105, 120, 32, 43, 61, 32, 34, 47, 47, 32, 69, 114, 114, 111, 114, 58, 32, 40, 34, 32, 43, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 91, 105, 93, 46, 115, 116, 97, 114, 116, 32, 43, 32, 34, 44, 34, 32, 43, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 91, 105, 93, 46, 108, 101, 110, 32, 43, 32, 34, 41, 32, 34, 32, 43, 32, 112, 97, 114, 115, 101, 69, 114, 114, 111, 114, 115, 91, 105, 93, 46, 109, 101, 115, 115, 97, 103, 101, 32, 43, 32, 34, 92, 114, 34, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 114, 101, 116, 117, 114, 110, 32, 101, 114, 114, 111, 114, 80, 114, 101, 102, 105, 120, 32, 43, 32, 111, 117, 116, 102, 105, 108, 101, 46, 115, 111, 117, 114, 99, 101, 59, 10, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 32, 32, 32, 32, 60, 115, 99, 114, 105, 112, 116, 32, 116, 121, 112, 101, 61, 34, 116, 101, 120, 116, 47, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 34, 62, 10, 9, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 108, 105, 98, 84, 101, 120, 116, 32, 61, 32, 34, 34, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 36, 46, 103, 101, 116, 40, 34, 46, 46, 47, 99, 111, 109, 112, 105, 108, 101, 114, 47, 108, 105, 98, 46, 116, 115, 34, 44, 32, 102, 117, 110, 99, 116, 105, 111, 110, 40, 110, 101, 119, 76, 105, 98, 84, 101, 120, 116, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 108, 105, 98, 84, 101, 120, 116, 32, 61, 32, 110, 101, 119, 76, 105, 98, 84, 101, 120, 116, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 41, 59, 9, 10, 32, 32, 32, 32, 32, 32, 32, 32, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 47, 47, 32, 101, 120, 101, 99, 117, 116, 101, 32, 116, 104, 101, 32, 106, 97, 118, 97, 115, 99, 114, 105, 112, 116, 32, 105, 110, 32, 116, 104, 101, 32, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 32, 112, 97, 110, 101, 10, 32, 32, 32, 32, 32, 32, 32, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 101, 120, 101, 99, 117, 116, 101, 40, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 40, 39, 35, 99, 111, 109, 112, 105, 108, 97, 116, 105, 111, 110, 39, 41, 46, 116, 101, 120, 116, 40, 34, 82, 117, 110, 110, 105, 110, 103, 46, 46, 46, 34, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 116, 120, 116, 32, 61, 32, 36, 40, 39, 35, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 39, 41, 46, 118, 97, 108, 40, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 114, 101, 115, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 114, 121, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 114, 101, 116, 32, 61, 32, 101, 118, 97, 108, 40, 116, 120, 116, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 114, 101, 115, 32, 61, 32, 34, 82, 97, 110, 32, 115, 117, 99, 99, 101, 115, 115, 102, 117, 108, 108, 121, 33, 34, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 32, 99, 97, 116, 99, 104, 40, 101, 41, 32, 123, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 114, 101, 115, 32, 61, 32, 34, 69, 120, 99, 101, 112, 116, 105, 111, 110, 32, 116, 104, 114, 111, 119, 110, 58, 32, 34, 32, 43, 32, 101, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 40, 39, 35, 99, 111, 109, 112, 105, 108, 97, 116, 105, 111, 110, 39, 41, 46, 116, 101, 120, 116, 40, 83, 116, 114, 105, 110, 103, 40, 114, 101, 115, 41, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 47, 47, 32, 114, 101, 99, 111, 109, 112, 105, 108, 101, 32, 116, 104, 101, 32, 115, 116, 114, 97, 100, 97, 83, 114, 99, 32, 97, 110, 100, 32, 112, 111, 112, 117, 108, 97, 116, 101, 32, 116, 104, 101, 32, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 32, 112, 97, 110, 101, 10, 32, 32, 32, 32, 32, 32, 32, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 115, 114, 99, 85, 112, 100, 97, 116, 101, 100, 40, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 110, 101, 119, 84, 101, 120, 116, 32, 61, 32, 36, 40, 39, 35, 115, 116, 114, 97, 100, 97, 83, 114, 99, 39, 41, 46, 118, 97, 108, 40, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 116, 114, 121, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 32, 61, 32, 99, 111, 109, 112, 105, 108, 101, 40, 110, 101, 119, 84, 101, 120, 116, 44, 32, 108, 105, 98, 84, 101, 120, 116, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 32, 99, 97, 116, 99, 104, 32, 40, 101, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 32, 61, 32, 34, 47, 47, 80, 97, 114, 115, 101, 32, 101, 114, 114, 111, 114, 34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 102, 111, 114, 40, 118, 97, 114, 32, 105, 32, 105, 110, 32, 101, 41, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 32, 43, 61, 32, 34, 92, 114, 47, 47, 32, 34, 32, 43, 32, 101, 91, 105, 93, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 40, 39, 35, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 39, 41, 46, 118, 97, 108, 40, 99, 111, 109, 112, 105, 108, 101, 100, 83, 111, 117, 114, 99, 101, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 32, 32, 32, 32, 47, 47, 32, 80, 111, 112, 117, 108, 97, 116, 101, 32, 116, 104, 101, 32, 115, 116, 114, 97, 100, 97, 83, 114, 99, 32, 112, 97, 110, 101, 32, 119, 105, 116, 104, 32, 111, 110, 101, 32, 111, 102, 32, 116, 104, 101, 32, 98, 117, 105, 108, 116, 32, 105, 110, 32, 115, 97, 109, 112, 108, 101, 115, 10, 32, 32, 32, 32, 32, 32, 32, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 101, 120, 97, 109, 112, 108, 101, 83, 101, 108, 101, 99, 116, 105, 111, 110, 67, 104, 97, 110, 103, 101, 100, 40, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 101, 120, 97, 109, 112, 108, 101, 115, 32, 61, 32, 100, 111, 99, 117, 109, 101, 110, 116, 46, 103, 101, 116, 69, 108, 101, 109, 101, 110, 116, 66, 121, 73, 100, 40, 39, 101, 120, 97, 109, 112, 108, 101, 115, 39, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 115, 101, 108, 101, 99, 116, 101, 100, 69, 120, 97, 109, 112, 108, 101, 32, 61, 32, 101, 120, 97, 109, 112, 108, 101, 115, 46, 111, 112, 116, 105, 111, 110, 115, 91, 101, 120, 97, 109, 112, 108, 101, 115, 46, 115, 101, 108, 101, 99, 116, 101, 100, 73, 110, 100, 101, 120, 93, 46, 118, 97, 108, 117, 101, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 105, 102, 32, 40, 115, 101, 108, 101, 99, 116, 101, 100, 69, 120, 97, 109, 112, 108, 101, 32, 33, 61, 32, 34, 34, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 46, 103, 101, 116, 40, 39, 101, 120, 97, 109, 112, 108, 101, 115, 47, 39, 32, 43, 32, 115, 101, 108, 101, 99, 116, 101, 100, 69, 120, 97, 109, 112, 108, 101, 44, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 40, 115, 114, 99, 84, 101, 120, 116, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 36, 40, 39, 35, 115, 116, 114, 97, 100, 97, 83, 114, 99, 39, 41, 46, 118, 97, 108, 40, 115, 114, 99, 84, 101, 120, 116, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 115, 101, 116, 84, 105, 109, 101, 111, 117, 116, 40, 115, 114, 99, 85, 112, 100, 97, 116, 101, 100, 44, 49, 48, 48, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 44, 32, 102, 117, 110, 99, 116, 105, 111, 110, 32, 40, 101, 114, 114, 41, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 99, 111, 110, 115, 111, 108, 101, 46, 108, 111, 103, 40, 101, 114, 114, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 10, 32, 32, 32, 32, 60, 47, 115, 99, 114, 105, 112, 116, 62, 10, 60, 47, 104, 101, 97, 100, 62, 10, 60, 98, 111, 100, 121, 62, 10, 32, 32, 32, 32, 60, 104, 49, 62, 84, 121, 112, 101, 83, 99, 114, 105, 112, 116, 60, 47, 104, 49, 62, 10, 32, 32, 32, 32, 60, 98, 114, 32, 47, 62, 10, 32, 32, 32, 32, 60, 115, 101, 108, 101, 99, 116, 32, 105, 100, 61, 34, 101, 120, 97, 109, 112, 108, 101, 115, 34, 32, 111, 110, 99, 104, 97, 110, 103, 101, 61, 39, 101, 120, 97, 109, 112, 108, 101, 83, 101, 108, 101, 99, 116, 105, 111, 110, 67, 104, 97, 110, 103, 101, 100, 40, 41, 39, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 34, 62, 83, 101, 108, 101, 99, 116, 46, 46, 46, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 115, 109, 97, 108, 108, 46, 116, 115, 34, 62, 83, 109, 97, 108, 108, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 101, 109, 112, 108, 111, 121, 101, 101, 46, 116, 115, 34, 62, 69, 109, 112, 108, 111, 121, 101, 101, 115, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 99, 111, 110, 119, 97, 121, 46, 116, 115, 34, 62, 67, 111, 110, 119, 97, 121, 32, 71, 97, 109, 101, 32, 111, 102, 32, 76, 105, 102, 101, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 111, 112, 116, 105, 111, 110, 32, 118, 97, 108, 117, 101, 61, 34, 116, 121, 112, 101, 115, 99, 114, 105, 112, 116, 46, 116, 115, 34, 62, 84, 121, 112, 101, 83, 99, 114, 105, 112, 116, 32, 67, 111, 109, 112, 105, 108, 101, 114, 60, 47, 111, 112, 116, 105, 111, 110, 62, 10, 32, 32, 32, 32, 60, 47, 115, 101, 108, 101, 99, 116, 62, 10, 10, 32, 32, 32, 32, 60, 100, 105, 118, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 116, 101, 120, 116, 97, 114, 101, 97, 32, 105, 100, 61, 39, 115, 116, 114, 97, 100, 97, 83, 114, 99, 39, 32, 114, 111, 119, 115, 61, 39, 52, 48, 39, 32, 99, 111, 108, 115, 61, 39, 56, 48, 39, 32, 111, 110, 99, 104, 97, 110, 103, 101, 61, 39, 115, 114, 99, 85, 112, 100, 97, 116, 101, 100, 40, 41, 39, 32, 111, 110, 107, 101, 121, 117, 112, 61, 39, 115, 114, 99, 85, 112, 100, 97, 116, 101, 100, 40, 41, 39, 32, 115, 112, 101, 108, 108, 99, 104, 101, 99, 107, 61, 34, 102, 97, 108, 115, 101, 34, 62, 10, 47, 47, 84, 121, 112, 101, 32, 121, 111, 117, 114, 32, 84, 121, 112, 101, 83, 99, 114, 105, 112, 116, 32, 104, 101, 114, 101, 46, 46, 46, 10, 32, 32, 32, 32, 32, 32, 60, 47, 116, 101, 120, 116, 97, 114, 101, 97, 62, 10, 32, 32, 32, 32, 32, 32, 60, 116, 101, 120, 116, 97, 114, 101, 97, 32, 105, 100, 61, 39, 99, 111, 109, 112, 105, 108, 101, 100, 79, 117, 116, 112, 117, 116, 39, 32, 114, 111, 119, 115, 61, 39, 52, 48, 39, 32, 99, 111, 108, 115, 61, 39, 56, 48, 39, 32, 115, 112, 101, 108, 108, 99, 104, 101, 99, 107, 61, 34, 102, 97, 108, 115, 101, 34, 62, 10, 47, 47, 67, 111, 109, 112, 105, 108, 101, 100, 32, 99, 111, 100, 101, 32, 119, 105, 108, 108, 32, 115, 104, 111, 119, 32, 117, 112, 32, 104, 101, 114, 101, 46, 46, 46, 10, 32, 32, 32, 32, 32, 32, 60, 47, 116, 101, 120, 116, 97, 114, 101, 97, 62, 10, 32, 32, 32, 32, 32, 32, 60, 98, 114, 32, 47, 62, 10, 32, 32, 32, 32, 32, 32, 60, 98, 117, 116, 116, 111, 110, 32, 111, 110, 99, 108, 105, 99, 107, 61, 39, 101, 120, 101, 99, 117, 116, 101, 40, 41, 39, 47, 62, 82, 117, 110, 60, 47, 98, 117, 116, 116, 111, 110, 62, 32, 10, 32, 32, 32, 32, 32, 32, 60, 100, 105, 118, 32, 105, 100, 61, 39, 99, 111, 109, 112, 105, 108, 97, 116, 105, 111, 110, 39, 62, 80, 114, 101, 115, 115, 32, 39, 114, 117, 110, 39, 32, 116, 111, 32, 101, 120, 101, 99, 117, 116, 101, 32, 99, 111, 100, 101, 46, 46, 46, 60, 47, 100, 105, 118, 62, 10, 32, 32, 32, 32, 32, 32, 60, 100, 105, 118, 32, 105, 100, 61, 39, 114, 101, 115, 117, 108, 116, 115, 39, 62, 46, 46, 46, 119, 114, 105, 116, 101, 32, 121, 111, 117, 114, 32, 114, 101, 115, 117, 108, 116, 115, 32, 105, 110, 116, 111, 32, 35, 114, 101, 115, 117, 108, 116, 115, 46, 46, 46, 60, 47, 100, 105, 118, 62, 10, 32, 32, 32, 32, 60, 47, 100, 105, 118, 62, 10, 32, 32, 32, 32, 60, 100, 105, 118, 32, 105, 100, 61, 39, 98, 111, 100, 39, 32, 115, 116, 121, 108, 101, 61, 39, 100, 105, 115, 112, 108, 97, 121, 58, 110, 111, 110, 101, 39, 62, 60, 47, 100, 105, 118, 62, 10, 60, 47, 98, 111, 100, 121, 62, 10, 60, 47, 104, 116, 109, 108, 62, 10 ]); +// This file is determined to be Windows-1252 unless candidateDetectEncoding is set +fixtures['some.shiftjis.1.txt'] = Uint8Array.from([82, 177, 82, 241, 82, 201, 82, 191, 82, 205]); + const lorem = getLorem(); // needle encoded from 'ÐБВГДЕЖЗИЙКЛМÐОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрÑтуфхцчшщъыьÑÑŽÑ' @@ -418,4 +421,3 @@ Donec vehicula mauris eget lacus mollis venenatis et sed nibh. Nam sodales ligul tail: VSBuffer.fromString(`Vivamus iaculis, lacus efficitur faucibus porta, dui nulla facilisis ligula, ut sodales odio nunc id sapien. Cras viverra auctor ipsum, dapibus mattis neque dictum sed. Sed convallis fermentum molestie. Nulla facilisi turpis duis.`) }; } - diff --git a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts index c01ba24353e03..47aa789ae2a95 100644 --- a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ITextFileService, snapshotToString, TextFileOperationError, TextFileOperationResult, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; import { URI } from 'vs/base/common/uri'; import { join, basename } from 'vs/base/common/path'; @@ -592,6 +592,21 @@ export default function createSuite(params: Params) { assert.strictEqual(result.encoding, 'windows1252'); }); + test('readStream - autoguessEncoding (candidateGuessEncodings)', async () => { + // This file is determined to be Windows-1252 unless candidateDetectEncoding is set. + const resource = URI.file(join(testDir, 'some.shiftjis.1.txt')); + + const result = await service.readStream(resource, { autoGuessEncoding: true, candidateGuessEncodings: ['utf-8', 'shiftjis', 'euc-jp'] }); + assert.strictEqual(result.encoding, 'shiftjis'); + }); + + test('readStream - autoguessEncoding (candidateGuessEncodings is Empty)', async () => { + const resource = URI.file(join(testDir, 'some_cp1252.txt')); + + const result = await service.readStream(resource, { autoGuessEncoding: true, candidateGuessEncodings: [] }); + assert.strictEqual(result.encoding, 'windows1252'); + }); + test('readStream - FILE_IS_BINARY', async () => { const resource = URI.file(join(testDir, 'binary.txt')); diff --git a/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts b/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts index a48728e4a7a31..a767ca64fba61 100644 --- a/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-sandbox/nativeTextFileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IFileService } from 'vs/platform/files/common/files'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; diff --git a/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts b/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts index b7da6009727fc..ebaeb4ab36c33 100644 --- a/src/vs/workbench/services/textfile/test/node/encoding/encoding.integrationTest.ts +++ b/src/vs/workbench/services/textfile/test/node/encoding/encoding.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 terminalEncoding from 'vs/base/node/terminalEncoding'; import * as encoding from 'vs/workbench/services/textfile/common/encoding'; diff --git a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts index f2a811f980774..6806b4e1c56f6 100644 --- a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts +++ b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts @@ -3,7 +3,7 @@ * Licensed 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 encoding from 'vs/workbench/services/textfile/common/encoding'; import * as streams from 'vs/base/common/stream'; @@ -208,6 +208,14 @@ suite('Encoding', () => { assert.strictEqual(mimes.encoding, 'windows1252'); }); + test('autoGuessEncoding (candidateGuessEncodings - ShiftJIS)', async function () { + // This file is determined to be windows1252 unless candidateDetectEncoding is set. + const file = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/some.shiftjis.1.txt').fsPath; + const buffer = await readExactlyByFile(file, 512 * 8); + const mimes = await encoding.detectEncodingFromBuffer(buffer, true, ['utf8', 'shiftjis', 'eucjp']); + assert.strictEqual(mimes.encoding, 'shiftjis'); + }); + async function readAndDecodeFromDisk(path: string, fileEncoding: string | null) { return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { @@ -246,7 +254,7 @@ suite('Encoding', () => { Buffer.from([65, 66, 67]), ]); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.ok(detected); assert.ok(stream); @@ -262,7 +270,7 @@ suite('Encoding', () => { Buffer.from([65, 66, 67]), ]); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 64, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 64, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.ok(detected); assert.ok(stream); @@ -275,7 +283,7 @@ suite('Encoding', () => { const source = newWriteableBufferStream(); source.end(); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 512, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 512, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.ok(detected); assert.ok(stream); @@ -288,7 +296,7 @@ suite('Encoding', () => { const path = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/some_utf16be.css').fsPath; const source = streamToBufferReadableStream(fs.createReadStream(path)); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 64, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 64, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.strictEqual(detected.encoding, 'utf16be'); assert.strictEqual(detected.seemsBinary, false); @@ -301,7 +309,7 @@ suite('Encoding', () => { test('toDecodeStream - empty file', async function () { const path = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/empty.txt').fsPath; const source = streamToBufferReadableStream(fs.createReadStream(path)); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); const expected = await readAndDecodeFromDisk(path, detected.encoding); const actual = await readAllAsString(stream); @@ -318,7 +326,7 @@ suite('Encoding', () => { } const source = newTestReadableStream(buffers); - const { stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); const expected = new TextDecoder().decode(incompleteEmojis); const actual = await readAllAsString(stream); @@ -330,7 +338,7 @@ suite('Encoding', () => { const path = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/some_gbk.txt').fsPath; const source = streamToBufferReadableStream(fs.createReadStream(path)); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async () => 'gbk' }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async () => 'gbk' }); assert.ok(detected); assert.ok(stream); @@ -342,7 +350,7 @@ suite('Encoding', () => { const path = FileAccess.asFileUri('vs/workbench/services/textfile/test/node/encoding/fixtures/issue_102202.txt').fsPath; const source = streamToBufferReadableStream(fs.createReadStream(path)); - const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async () => 'utf-8' }); + const { detected, stream } = await encoding.toDecodeStream(source, { acceptTextOnly: true, minBytesRequiredForDetection: 4, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async () => 'utf-8' }); assert.ok(detected); assert.ok(stream); @@ -365,7 +373,7 @@ suite('Encoding', () => { let error: Error | undefined = undefined; try { - await encoding.toDecodeStream(source(), { acceptTextOnly: true, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + await encoding.toDecodeStream(source(), { acceptTextOnly: true, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); } catch (e) { error = e; } @@ -375,7 +383,7 @@ suite('Encoding', () => { // acceptTextOnly: false - const { detected, stream } = await encoding.toDecodeStream(source(), { acceptTextOnly: false, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 }); + const { detected, stream } = await encoding.toDecodeStream(source(), { acceptTextOnly: false, guessEncoding: false, candidateGuessEncodings: [], overwriteEncoding: async detected => detected || encoding.UTF8 }); assert.ok(detected); assert.strictEqual(detected.seemsBinary, true); diff --git a/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some.shiftjis.1.txt b/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some.shiftjis.1.txt new file mode 100644 index 0000000000000..67acab8795e4b --- /dev/null +++ b/src/vs/workbench/services/textfile/test/node/encoding/fixtures/some.shiftjis.1.txt @@ -0,0 +1,2 @@ +// This file is determined to be Windows-1252 unless candidateDetectEncoding is set. +‚±‚ñ‚É‚¿‚Í \ No newline at end of file diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts index f38d12fc2bbdd..34d9c854314a9 100644 --- a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { ITextModel } from 'vs/editor/common/model'; import { URI } from 'vs/base/common/uri'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index ec8f4a607cd1b..ae2c1c6cd0b22 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -465,6 +465,6 @@ function handleParentFolder(key: string, selectors: string[]): string { } function escapeCSS(str: string) { - str = 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. + str = 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. return mainWindow.CSS.escape(str); } diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index eb40c8d5806ff..327b1d9133ad8 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -136,6 +136,8 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme this.currentProductIconTheme = ProductIconThemeData.createUnloadedTheme(''); this.productIconThemeSequencer = new Sequencer(); + this._register(this.onDidColorThemeChange(theme => getColorRegistry().notifyThemeUpdate(theme))); + // In order to avoid paint flashing for tokens, because // themes are loaded asynchronously, we need to initialize // a color theme document with good defaults until the theme is loaded diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index dd7a8f0d03f11..e74a4ab1e05f0 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -11,7 +11,7 @@ import { convertSettings } from 'vs/workbench/services/themes/common/themeCompat import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import * as resources from 'vs/base/common/resources'; -import { Extensions as ColorRegistryExtensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { Extensions as ColorRegistryExtensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground, DEFAULT_COLOR_CONFIG_VALUE } from 'vs/platform/theme/common/colorRegistry'; import { ITokenStyle, getThemeTypeSelector } from 'vs/platform/theme/common/themeService'; import { Registry } from 'vs/platform/registry/common/platform'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; @@ -45,6 +45,10 @@ export type TokenStyleDefinitions = { [P in keyof TokenStyleData]?: TokenStyleDe export type TextMateThemingRuleDefinitions = { [P in keyof TokenStyleData]?: ITextMateThemingRule | undefined; } & { scope?: ProbeScope }; +interface IColorOrDefaultMap { + [id: string]: Color | typeof DEFAULT_COLOR_CONFIG_VALUE; +} + export class ColorThemeData implements IWorkbenchColorTheme { static readonly STORAGE_KEY = 'colorThemeData'; @@ -65,7 +69,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { private themeTokenColors: ITextMateThemingRule[] = []; private customTokenColors: ITextMateThemingRule[] = []; private colorMap: IColorMap = {}; - private customColorMap: IColorMap = {}; + private customColorMap: IColorOrDefaultMap = {}; private semanticTokenRules: SemanticTokenRule[] = []; private customSemanticTokenRules: SemanticTokenRule[] = []; @@ -132,15 +136,20 @@ export class ColorThemeData implements IWorkbenchColorTheme { } public getColor(colorId: ColorIdentifier, useDefault?: boolean): Color | undefined { - let color: Color | undefined = this.customColorMap[colorId]; - if (color) { - return color; + const customColor = this.customColorMap[colorId]; + if (customColor instanceof Color) { + return customColor; + } + if (customColor === undefined) { /* !== DEFAULT_COLOR_CONFIG_VALUE */ + const color = this.colorMap[colorId]; + if (color !== undefined) { + return color; + } } - color = this.colorMap[colorId]; - if (useDefault !== false && types.isUndefined(color)) { - color = this.getDefault(colorId); + if (useDefault !== false) { + return this.getDefault(colorId); } - return color; + return undefined; } private getTokenStyle(type: string, modifiers: string[], language: string, useDefault = true, definitions: TokenStyleDefinitions = {}): TokenStyle | undefined { @@ -346,7 +355,11 @@ export class ColorThemeData implements IWorkbenchColorTheme { } public defines(colorId: ColorIdentifier): boolean { - return this.customColorMap.hasOwnProperty(colorId) || this.colorMap.hasOwnProperty(colorId); + const customColor = this.customColorMap[colorId]; + if (customColor instanceof Color) { + return true; + } + return customColor === undefined /* !== DEFAULT_COLOR_CONFIG_VALUE */ && this.colorMap.hasOwnProperty(colorId); } public setCustomizations(settings: ThemeConfiguration) { @@ -372,7 +385,9 @@ export class ColorThemeData implements IWorkbenchColorTheme { private overwriteCustomColors(colors: IColorCustomizations) { for (const id in colors) { const colorVal = colors[id]; - if (typeof colorVal === 'string') { + if (colorVal === DEFAULT_COLOR_CONFIG_VALUE) { + this.customColorMap[id] = DEFAULT_COLOR_CONFIG_VALUE; + } else if (typeof colorVal === 'string') { this.customColorMap[id] = Color.fromHex(colorVal); } } @@ -716,8 +731,10 @@ async function _loadColorTheme(extensionResourceLoaderService: IExtensionResourc } // new JSON color themes format for (const colorId in colors) { - const colorHex = colors[colorId]; - if (typeof colorHex === 'string') { // ignore colors tht are null + const colorVal = colors[colorId]; + if (colorVal === DEFAULT_COLOR_CONFIG_VALUE) { // ignore colors that are set to to default + delete result.colors[colorId]; + } else if (typeof colorVal === 'string') { result.colors[colorId] = Color.fromHex(colors[colorId]); } } diff --git a/src/vs/workbench/services/themes/test/node/tokenStyleResolving.test.ts b/src/vs/workbench/services/themes/test/node/tokenStyleResolving.test.ts index 98701b3dadadb..3376ac6e47d83 100644 --- a/src/vs/workbench/services/themes/test/node/tokenStyleResolving.test.ts +++ b/src/vs/workbench/services/themes/test/node/tokenStyleResolving.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData'; -import * as assert from 'assert'; +import assert from 'assert'; import { ITokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { TokenStyle, getTokenClassificationRegistry } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { Color } from 'vs/base/common/color'; diff --git a/src/vs/workbench/services/timer/browser/timerService.ts b/src/vs/workbench/services/timer/browser/timerService.ts index df14ee528cc4f..486180f41f860 100644 --- a/src/vs/workbench/services/timer/browser/timerService.ts +++ b/src/vs/workbench/services/timer/browser/timerService.ts @@ -566,8 +566,7 @@ export abstract class AbstractTimerService implements ITimerService { const t1 = performance.now(); fib(24); const value = Math.round(performance.now() - t1); - // eslint-disable-next-line no-restricted-globals - postMessage({ value: tooSlow ? -1 : value }); + self.postMessage({ value: tooSlow ? -1 : value }); }).toString(); diff --git a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts index 1794885179731..794fb8960ce82 100644 --- a/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts +++ b/src/vs/workbench/services/tunnel/electron-sandbox/tunnelService.ts @@ -68,11 +68,11 @@ export class TunnelService extends AbstractTunnelService { super(logService, configurationService); // Destroy any shared process tunnels that might still be active - lifecycleService.onDidShutdown(() => { + this._register(lifecycleService.onDidShutdown(() => { this._activeSharedProcessTunnels.forEach((id) => { this._sharedProcessTunnelService.destroyTunnel(id); }); - }); + })); } public isPortPrivileged(port: number): boolean { diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index 43402bc496ced..46563d9317dbc 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -70,7 +70,7 @@ export interface IUntitledTextEditorModel extends ITextEditorModel, ILanguageSup export class UntitledTextEditorModel extends BaseTextEditorModel implements IUntitledTextEditorModel { private static readonly FIRST_LINE_NAME_MAX_LENGTH = 40; - private static readonly FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH = UntitledTextEditorModel.FIRST_LINE_NAME_MAX_LENGTH * 10; + private static readonly FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH = this.FIRST_LINE_NAME_MAX_LENGTH * 10; // Support the special '${activeEditorLanguage}' language by // looking up the language id from the editor that is active diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts index f64d8c7e4c835..35936c00f50e2 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.integrationTest.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 9ccf8fa049a06..1ccafa64c8106 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { join } from 'vs/base/common/path'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts b/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts index f97c4ab35ba48..94c72c799df49 100644 --- a/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts +++ b/src/vs/workbench/services/userActivity/test/browser/domActivityTracker.test.ts @@ -7,7 +7,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { DomActivityTracker } from 'vs/workbench/services/userActivity/browser/domActivityTracker'; import { UserActivityService } from 'vs/workbench/services/userActivity/common/userActivityService'; import * as sinon from 'sinon'; -import * as assert from 'assert'; +import assert from 'assert'; suite('DomActivityTracker', () => { let uas: UserActivityService; diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts index 06c54a5729cc2..6173c8498f893 100644 --- a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; @@ -114,7 +115,7 @@ export class ExtensionsResource implements IProfileResource { return JSON.stringify(exclude?.length ? extensions.filter(e => !exclude.includes(e.identifier.id.toLowerCase())) : extensions); } - async apply(content: string, profile: IUserDataProfile): Promise { + async apply(content: string, profile: IUserDataProfile, progress?: (message: string) => void, token?: CancellationToken): Promise { return this.withProfileScopedServices(profile, async (extensionEnablementService) => { const profileExtensions: IProfileExtension[] = await this.getProfileExtensions(content); const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); @@ -168,7 +169,17 @@ export class ExtensionsResource implements IProfileResource { } })); if (installExtensionInfos.length) { - await this.extensionManagementService.installGalleryExtensions(installExtensionInfos); + if (token) { + for (const installExtensionInfo of installExtensionInfos) { + if (token.isCancellationRequested) { + return; + } + progress?.(localize('installingExtension', "Installing extension {0}...", installExtensionInfo.extension.displayName ?? installExtensionInfo.extension.identifier.id)); + await this.extensionManagementService.installFromGallery(installExtensionInfo.extension, installExtensionInfo.options); + } + } else { + await this.extensionManagementService.installGalleryExtensions(installExtensionInfos); + } } this.logService.info(`Importing Profile (${profile.name}): Finished installing extensions.`); } @@ -284,6 +295,7 @@ export abstract class ExtensionsResourceTreeItem implements IProfileResourceTree label: localize('exclude', "Select {0} Extension", e.displayName || e.identifier.id), } } : undefined, + themeIcon: Codicon.extensions, command: { id: 'extension.open', title: '', diff --git a/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts b/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts index 510e58bcfe051..c5ed415786ac6 100644 --- a/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts @@ -84,7 +84,7 @@ export class TasksResourceTreeItem implements IProfileResourceTreeItem { readonly type = ProfileResourceType.Tasks; readonly handle = ProfileResourceType.Tasks; - readonly label = { label: localize('tasks', "User Tasks") }; + readonly label = { label: localize('tasks', "Tasks") }; readonly collapsibleState = TreeItemCollapsibleState.Expanded; checkbox: ITreeItemCheckboxState | undefined; diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index 13b592b7e0e14..7c5bab40b8741 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -24,7 +24,6 @@ import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/con import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { ILogService } from 'vs/platform/log/common/log'; import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { SettingsResource, SettingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/settingsResource'; import { KeybindingsResource, KeybindingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/keybindingsResource'; @@ -64,7 +63,7 @@ import { Action, ActionRunner, IAction, IActionRunner } from 'vs/base/common/act import { isWeb } from 'vs/base/common/platform'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { Codicon, getAllCodicons } from 'vs/base/common/codicons'; -import { Barrier } from 'vs/base/common/async'; +import { Barrier, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -81,6 +80,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import type { IHoverWidget } from 'vs/base/browser/ui/hover/hover'; import { IAccessibleViewInformationService } from 'vs/workbench/services/accessibility/common/accessibleViewInformationService'; +import { ILogService } from 'vs/platform/log/common/log'; interface IUserDataProfileTemplate { readonly name: string; @@ -193,7 +193,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU this.profileContentHandlers.delete(id); } - async exportProfile(): Promise { + async exportProfile2(): Promise { if (this.isProfileExportInProgressContextKey.get()) { this.logService.warn('Profile export already in progress.'); return; @@ -225,7 +225,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU if (mode === 'preview') { await this.previewProfile(profileTemplate, options); } else if (mode === 'apply') { - await this.createAndSwitch(profileTemplate, false, true, options, localize('create profile', "Create Profile")); + await this.createAndSwitch(profileTemplate, !!options?.transient, true, options, localize('create profile', "Create Profile")); } else if (mode === 'both') { await this.importAndPreviewProfile(uri, profileTemplate, options); } @@ -242,6 +242,123 @@ export class UserDataProfileImportExportService extends Disposable implements IU return this.saveProfile(profile); } + async createFromProfile(from: IUserDataProfile, options: IUserDataProfileCreateOptions, token: CancellationToken): Promise { + const disposables = new DisposableStore(); + let creationPromise: CancelablePromise; + disposables.add(token.onCancellationRequested(() => creationPromise.cancel())); + let profile: IUserDataProfile | undefined; + return this.progressService.withProgress({ + location: ProgressLocation.Notification, + delay: 500, + sticky: true, + cancellable: true, + }, async progress => { + const reportProgress = (message: string) => progress.report({ message: localize('create from profile', "Create Profile: {0}", message) }); + creationPromise = createCancelablePromise(async token => { + const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, from, { ...options?.resourceTypeFlags, extensions: false })); + const profileTemplate = await userDataProfilesExportState.getProfileTemplate(options.name ?? from.name, options?.icon); + profile = await this.getProfileToImport({ ...profileTemplate, name: options.name ?? profileTemplate.name }, !!options.transient, options); + if (!profile) { + return; + } + if (token.isCancellationRequested) { + return; + } + await this.applyProfileTemplate(profileTemplate, profile, options, reportProgress, token); + }); + try { + await creationPromise; + if (profile && (options?.resourceTypeFlags?.extensions ?? true)) { + reportProgress(localize('installing extensions', "Installing Extensions...")); + await this.instantiationService.createInstance(ExtensionsResource).copy(from, profile, false); + } + } catch (error) { + if (profile) { + await this.userDataProfilesService.removeProfile(profile); + profile = undefined; + } + } + return profile; + + }, () => creationPromise.cancel()).finally(() => disposables.dispose()); + } + + async createProfileFromTemplate(profileTemplate: IUserDataProfileTemplate, options: IUserDataProfileCreateOptions, token: CancellationToken): Promise { + const disposables = new DisposableStore(); + let creationPromise: CancelablePromise; + disposables.add(token.onCancellationRequested(() => creationPromise.cancel())); + let profile: IUserDataProfile | undefined; + return this.progressService.withProgress({ + location: ProgressLocation.Notification, + delay: 500, + sticky: true, + cancellable: true, + }, async progress => { + const reportProgress = (message: string) => progress.report({ message: localize('create from profile', "Create Profile: {0}", message) }); + creationPromise = createCancelablePromise(async token => { + profile = await this.getProfileToImport({ ...profileTemplate, name: options.name ?? profileTemplate.name }, !!options.transient, options); + if (!profile) { + return; + } + if (token.isCancellationRequested) { + return; + } + await this.applyProfileTemplate(profileTemplate, profile, options, reportProgress, token); + }); + try { + await creationPromise; + } catch (error) { + if (profile) { + await this.userDataProfilesService.removeProfile(profile); + profile = undefined; + } + } + return profile; + }, () => creationPromise.cancel()).finally(() => disposables.dispose()); + } + + private async applyProfileTemplate(profileTemplate: IUserDataProfileTemplate, profile: IUserDataProfile, options: IUserDataProfileCreateOptions, reportProgress: (message: string) => void, token: CancellationToken): Promise { + if (profileTemplate.settings && (options.resourceTypeFlags?.settings ?? true) && !profile.useDefaultFlags?.settings) { + reportProgress(localize('creating settings', "Creating Settings...")); + await this.instantiationService.createInstance(SettingsResource).apply(profileTemplate.settings, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.keybindings && (options.resourceTypeFlags?.keybindings ?? true) && !profile.useDefaultFlags?.keybindings) { + reportProgress(localize('create keybindings', "Creating Keyboard Shortcuts...")); + await this.instantiationService.createInstance(KeybindingsResource).apply(profileTemplate.keybindings, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.tasks && (options.resourceTypeFlags?.tasks ?? true) && !profile.useDefaultFlags?.tasks) { + reportProgress(localize('create tasks', "Creating Tasks...")); + await this.instantiationService.createInstance(TasksResource).apply(profileTemplate.tasks, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.snippets && (options.resourceTypeFlags?.snippets ?? true) && !profile.useDefaultFlags?.snippets) { + reportProgress(localize('create snippets', "Creating Snippets...")); + await this.instantiationService.createInstance(SnippetsResource).apply(profileTemplate.snippets, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.globalState && !profile.useDefaultFlags?.globalState) { + reportProgress(localize('applying global state', "Applying UI State...")); + await this.instantiationService.createInstance(GlobalStateResource).apply(profileTemplate.globalState, profile); + } + if (token.isCancellationRequested) { + return; + } + if (profileTemplate.extensions && (options.resourceTypeFlags?.extensions ?? true) && !profile.useDefaultFlags?.extensions) { + reportProgress(localize('installing extensions', "Installing Extensions...")); + await this.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, profile, reportProgress, token); + } + } + private saveProfile(profile: IUserDataProfile): Promise; private saveProfile(profile?: IUserDataProfile, source?: IUserDataProfile | URI | IUserDataProfileTemplate): Promise; private async saveProfile(profile?: IUserDataProfile, source?: IUserDataProfile | URI | Mutable): Promise { @@ -271,8 +388,8 @@ export class UserDataProfileImportExportService extends Disposable implements IU const settings: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Settings, label: localize('settings', "Settings"), picked: !profile?.useDefaultFlags?.settings }; const keybindings: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Keybindings, label: localize('keybindings', "Keyboard Shortcuts"), picked: !profile?.useDefaultFlags?.keybindings }; - const snippets: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Snippets, label: localize('snippets', "User Snippets"), picked: !profile?.useDefaultFlags?.snippets }; - const tasks: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Tasks, label: localize('tasks', "User Tasks"), picked: !profile?.useDefaultFlags?.tasks }; + const snippets: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Snippets, label: localize('snippets', "Snippets"), picked: !profile?.useDefaultFlags?.snippets }; + const tasks: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Tasks, label: localize('tasks', "Tasks"), picked: !profile?.useDefaultFlags?.tasks }; const extensions: IQuickPickItem & { id: ProfileResourceType } = { id: ProfileResourceType.Extensions, label: localize('extensions', "Extensions"), picked: !profile?.useDefaultFlags?.extensions }; const resources = [settings, keybindings, snippets, tasks, extensions]; @@ -527,7 +644,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU await this.importProfile(source, { mode: 'apply', name: result.name, useDefaultFlags, icon: result.icon ? result.icon : undefined }); } else if (isUserDataProfile(source)) { this.telemetryService.publicLog2('userDataProfile.createFromProfile', createProfileTelemetryData); - await this.createFromProfile(source, result.name, { useDefaultFlags, icon: result.icon ? result.icon : undefined }); + await this._createFromProfile(source, result.name, { useDefaultFlags, icon: result.icon ? result.icon : undefined }); } else if (isUserDataProfileTemplate(source)) { source.name = result.name; this.telemetryService.publicLog2('userDataProfile.createFromExternalTemplate', createProfileTelemetryData); @@ -572,7 +689,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - async exportProfile2(profile: IUserDataProfile): Promise { + async exportProfile(profile: IUserDataProfile): Promise { const disposables = new DisposableStore(); try { const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, profile, undefined)); @@ -582,7 +699,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - async createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileCreateOptions): Promise { + private async _createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileCreateOptions): Promise { const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, profile, options?.resourceTypeFlags); try { const profileTemplate = await userDataProfilesExportState.getProfileTemplate(name, options?.icon); @@ -592,13 +709,12 @@ export class UserDataProfileImportExportService extends Disposable implements IU sticky: true, }, async progress => { const reportProgress = (message: string) => progress.report({ message: localize('create from profile', "Create Profile: {0}", message) }); - const createdProfile = await this.doCreateProfile(profileTemplate, false, false, { useDefaultFlags: options?.useDefaultFlags, icon: options?.icon }, reportProgress); + const createdProfile = await this.doCreateProfile(profileTemplate, false, false, { useDefaultFlags: options?.useDefaultFlags, icon: options?.icon, transient: options?.transient }, reportProgress); if (createdProfile) { if (options?.resourceTypeFlags?.extensions ?? true) { reportProgress(localize('progress extensions', "Applying Extensions...")); await this.instantiationService.createInstance(ExtensionsResource).copy(profile, createdProfile, false); } - reportProgress(localize('switching profile', "Switching Profile...")); await this.userDataProfileManagementService.switchProfile(createdProfile); } @@ -1068,24 +1184,6 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - async setProfile(profile: IUserDataProfileTemplate): Promise { - await this.progressService.withProgress({ - location: ProgressLocation.Notification, - title: localize('profiles.applying', "{0}: Applying...", PROFILES_CATEGORY.value), - }, async progress => { - if (profile.settings) { - await this.instantiationService.createInstance(SettingsResource).apply(profile.settings, this.userDataProfileService.currentProfile); - } - if (profile.globalState) { - await this.instantiationService.createInstance(GlobalStateResource).apply(profile.globalState, this.userDataProfileService.currentProfile); - } - if (profile.extensions) { - await this.instantiationService.createInstance(ExtensionsResource).apply(profile.extensions, this.userDataProfileService.currentProfile); - } - }); - this.notificationService.info(localize('applied profile', "{0}: Applied successfully.", PROFILES_CATEGORY.value)); - } - } class FileUserDataProfileContentHandler implements IUserDataProfileContentHandler { diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index e454c34038ad9..ff845818b8db3 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -79,6 +79,10 @@ export class UserDataProfileManagementService extends Disposable implements IUse } } + async createProfile(name: string, options?: IUserDataProfileOptions): Promise { + return this.userDataProfilesService.createNamedProfile(name, options); + } + async createAndEnterProfile(name: string, options?: IUserDataProfileOptions): Promise { const profile = await this.userDataProfilesService.createNamedProfile(name, options, toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())); await this.changeCurrentProfile(profile); @@ -93,15 +97,16 @@ export class UserDataProfileManagementService extends Disposable implements IUse return profile; } - async updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise { + async updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise { if (!this.userDataProfilesService.profiles.some(p => p.id === profile.id)) { throw new Error(`Profile ${profile.name} does not exist`); } if (profile.isDefault) { throw new Error(localize('cannotRenameDefaultProfile', "Cannot rename the default profile")); } - await this.userDataProfilesService.updateProfile(profile, updateOptions); + const updatedProfile = await this.userDataProfilesService.updateProfile(profile, updateOptions); this.telemetryService.publicLog2('profileManagementActionExecuted', { id: 'updateProfile' }); + return updatedProfile; } async removeProfile(profile: IUserDataProfile): Promise { diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts index 3dfaaf20e4c37..24a18be859876 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts @@ -24,7 +24,7 @@ export class UserDataProfileStorageService extends AbstractUserDataProfileStorag @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @ILogService private readonly logService: ILogService, ) { - super(storageService); + super(true, storageService); const disposables = this._register(new DisposableStore()); this._register(Event.filter(storageService.onDidChangeTarget, e => e.scope === StorageScope.PROFILE, disposables)(() => this.onDidChangeStorageTargetInCurrentProfile())); this._register(storageService.onDidChangeValue(StorageScope.PROFILE, undefined, disposables)(e => this.onDidChangeStorageValueInCurrentProfile(e))); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index d2c5cb4487eb7..e5c260e0510f6 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -42,16 +42,19 @@ export const IUserDataProfileManagementService = createDecorator; createAndEnterProfile(name: string, options?: IUserDataProfileOptions): Promise; createAndEnterTransientProfile(): Promise; removeProfile(profile: IUserDataProfile): Promise; - updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise; + updateProfile(profile: IUserDataProfile, updateOptions: IUserDataProfileUpdateOptions): Promise; switchProfile(profile: IUserDataProfile): Promise; getBuiltinProfileTemplates(): Promise; } export interface IUserDataProfileTemplate { + readonly name: string; + readonly icon?: string; readonly settings?: string; readonly keybindings?: string; readonly tasks?: string; @@ -78,15 +81,15 @@ export function toUserDataProfileUri(path: string, productService: IProductServi }); } -export interface IProfileImportOptions extends IUserDataProfileOptions { +export interface IUserDataProfileCreateOptions extends IUserDataProfileOptions { readonly name?: string; - readonly icon?: string; - readonly mode?: 'preview' | 'apply' | 'both'; readonly resourceTypeFlags?: ProfileResourceTypeFlags; } -export interface IUserDataProfileCreateOptions extends IUserDataProfileOptions { - readonly resourceTypeFlags?: ProfileResourceTypeFlags; +export interface IProfileImportOptions extends IUserDataProfileCreateOptions { + readonly name?: string; + readonly icon?: string; + readonly mode?: 'preview' | 'apply' | 'both'; } export const IUserDataProfileImportExportService = createDecorator('IUserDataProfileImportExportService'); @@ -97,15 +100,15 @@ export interface IUserDataProfileImportExportService { unregisterProfileContentHandler(id: string): void; resolveProfileTemplate(uri: URI): Promise; - exportProfile(): Promise; - exportProfile2(profile: IUserDataProfile): Promise; + exportProfile(profile: IUserDataProfile): Promise; + exportProfile2(): Promise; importProfile(uri: URI, options?: IProfileImportOptions): Promise; showProfileContents(): Promise; createProfile(from?: IUserDataProfile | URI): Promise; - createFromProfile(from: IUserDataProfile, name: string, options?: IUserDataProfileCreateOptions): Promise; editProfile(profile: IUserDataProfile): Promise; + createFromProfile(from: IUserDataProfile, options: IUserDataProfileCreateOptions, token: CancellationToken): Promise; + createProfileFromTemplate(profileTemplate: IUserDataProfileTemplate, options: IUserDataProfileCreateOptions, token: CancellationToken): Promise; createTroubleshootProfile(): Promise; - setProfile(profile: IUserDataProfileTemplate): Promise; } export interface IProfileResourceInitializer { diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts index 93251b9b1c34f..6e0e28a0347b2 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileIcons.ts @@ -71,7 +71,7 @@ export const ICONS = [ Codicon.pulse, Codicon.radioTower, Codicon.smiley, - Codicon.symbolEvent, + Codicon.zap, Codicon.squirrel, Codicon.symbolColor, Codicon.mail, diff --git a/src/vs/workbench/services/userDataSync/common/userDataSync.ts b/src/vs/workbench/services/userDataSync/common/userDataSync.ts index 9aa88c9e1d088..759011c2549f1 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSync.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSync.ts @@ -55,8 +55,8 @@ export function getSyncAreaLabel(source: SyncResource): string { switch (source) { case SyncResource.Settings: return localize('settings', "Settings"); case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); - case SyncResource.Snippets: return localize('snippets', "User Snippets"); - case SyncResource.Tasks: return localize('tasks', "User Tasks"); + case SyncResource.Snippets: return localize('snippets', "Snippets"); + case SyncResource.Tasks: return localize('tasks', "Tasks"); case SyncResource.Extensions: return localize('extensions', "Extensions"); case SyncResource.GlobalState: return localize('ui state label', "UI State"); case SyncResource.Profiles: return localize('profiles', "Profiles"); diff --git a/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts b/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts index 45329df40c480..cd7f253b913ff 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts @@ -23,8 +23,8 @@ class UserDataSyncUtilService implements IUserDataSyncUtilService { @ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService, ) { } - async resolveDefaultIgnoredSettings(): Promise { - return getDefaultIgnoredSettings(); + async resolveDefaultCoreIgnoredSettings(): Promise { + return getDefaultIgnoredSettings(true); } async resolveUserBindings(userBindings: string[]): Promise> { diff --git a/src/vs/workbench/services/views/browser/viewsService.ts b/src/vs/workbench/services/views/browser/viewsService.ts index c342e91da150a..0a7d3a2f760b1 100644 --- a/src/vs/workbench/services/views/browser/viewsService.ts +++ b/src/vs/workbench/services/views/browser/viewsService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable, DisposableStore, DisposableMap } from 'vs/base/common/lifecycle'; import { IViewDescriptorService, ViewContainer, IViewDescriptor, IView, ViewContainerLocation, IViewPaneContainer } from 'vs/workbench/common/views'; import { FocusedViewContext, getVisbileViewContextKey } from 'vs/workbench/common/contextkeys'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -51,6 +51,7 @@ export class ViewsService extends Disposable implements IViewsService { private readonly _onDidChangeFocusedView = this._register(new Emitter()); readonly onDidChangeFocusedView = this._onDidChangeFocusedView.event; + private readonly viewContainerDisposables = this._register(new DisposableMap()); private readonly enabledViewContainersContextKeys: Map>; private readonly visibleViewContextKeys: Map>; private readonly focusedViewContextKey: IContextKey; @@ -114,7 +115,7 @@ export class ViewsService extends Disposable implements IViewsService { private onDidChangeContainers(added: ReadonlyArray<{ container: ViewContainer; location: ViewContainerLocation }>, removed: ReadonlyArray<{ container: ViewContainer; location: ViewContainerLocation }>): void { for (const { container, location } of removed) { - this.deregisterPaneComposite(container, location); + this.onDidDeregisterViewContainer(container, location); } for (const { container, location } of added) { this.onDidRegisterViewContainer(container, location); @@ -123,15 +124,24 @@ export class ViewsService extends Disposable implements IViewsService { private onDidRegisterViewContainer(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { this.registerPaneComposite(viewContainer, viewContainerLocation); + const disposables = new DisposableStore(); + const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); this.onViewDescriptorsAdded(viewContainerModel.allViewDescriptors, viewContainer); - this._register(viewContainerModel.onDidChangeAllViewDescriptors(({ added, removed }) => { + disposables.add(viewContainerModel.onDidChangeAllViewDescriptors(({ added, removed }) => { this.onViewDescriptorsAdded(added, viewContainer); this.onViewDescriptorsRemoved(removed); })); this.updateViewContainerEnablementContextKey(viewContainer); - this._register(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.updateViewContainerEnablementContextKey(viewContainer))); - this._register(this.registerOpenViewContainerAction(viewContainer)); + disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.updateViewContainerEnablementContextKey(viewContainer))); + disposables.add(this.registerOpenViewContainerAction(viewContainer)); + + this.viewContainerDisposables.set(viewContainer.id, disposables); + } + + private onDidDeregisterViewContainer(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { + this.deregisterPaneComposite(viewContainer, viewContainerLocation); + this.viewContainerDisposables.deleteAndDispose(viewContainer.id); } private onDidChangeContainerLocation(viewContainer: ViewContainer, from: ViewContainerLocation, to: ViewContainerLocation): void { diff --git a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts index f37d4af42b73f..c20ad890065c7 100644 --- a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as assert from 'assert'; +import assert from 'assert'; import * as sinon from 'sinon'; import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewContainerModel, IViewDescriptorService, ViewContainer } from 'vs/workbench/common/views'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts index 31b8bb8a6a335..f9ab97f41e2d3 100644 --- a/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as assert from 'assert'; +import assert from 'assert'; import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, ViewContainer, ViewContainerLocationToString } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index 7d23dd40fea56..0c3532605b9dd 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -935,8 +935,7 @@ export class StoredFileWorkingCopy extend title: localize('saveParticipants', "Saving '{0}'", this.name), location: ProgressLocation.Window, cancellable: true, - delay: this.isDirty() ? 3000 : 5000, - type: 'loading' + delay: this.isDirty() ? 3000 : 5000 }, progress => { return this.doSaveSequential(versionId, options, progress, saveCancellation); }, () => { diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 2988bbb28dc04..be93a5dea1f5c 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, TestInMemoryFileSystemProvider } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts index fee71d4e7d07b..527e47deac1e4 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { CancellationToken } from 'vs/base/common/cancellation'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index ab5922a8e16f4..33c986d3432f0 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -3,7 +3,7 @@ * Licensed 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, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, isStoredFileWorkingCopySaveEvent, IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts index fc20107d8107d..08dc69d5ae5e4 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index 42177887afd29..5b9239849f7c3 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBufferReadableStream, newWriteableBufferStream, VSBuffer, streamToBuffer, bufferToStream, readableToBuffer, VSBufferReadable } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index e123722055477..45e86fd3d38c7 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts index a04a7c4951ac8..91926cb177481 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { VSBufferReadableStream, VSBuffer, streamToBuffer, bufferToStream, readableToBuffer, VSBufferReadable } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts index 1e741e7bc9ee9..5dff0762979d0 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts index 9821fae2c4a2f..e759ceb0d82bb 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyEditorService.test.ts @@ -3,7 +3,7 @@ * Licensed 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/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts index af1ee0b5b00c7..a6458af7ac947 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 384e3b1795561..e0214cd2d79ea 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { URI } from 'vs/base/common/uri'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts index b4ee3100922e6..daa2930e27a11 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupService.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { insert } from 'vs/base/common/arrays'; import { hash } from 'vs/base/common/hash'; @@ -71,6 +71,10 @@ const TestNativeWindowConfiguration: INativeWindowConfiguration = { tmpDir: tmpDir.fsPath, userDataDir: joinPath(homeDir, product.nameShort).fsPath, profiles: { profile: NULL_PROFILE, all: [NULL_PROFILE], home: homeDir }, + nls: { + messages: [], + language: 'en' + }, _: [] }; diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts index fa293b361cf92..ae14f5d12d438 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts index 40a3f059f9b3b..83bd0aeebad34 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryService.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TestContextService, TestStorageService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { NullLogService } from 'vs/platform/log/common/log'; import { FileService } from 'vs/platform/files/common/fileService'; diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts index ef04d7041db52..b749708ada9c5 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyHistoryTracker.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { TestContextService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { randomPath } from 'vs/base/common/extpath'; diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index 21312c7e76cd3..a3be20f527712 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -29,8 +29,9 @@ import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/w import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { Disposable } from 'vs/base/common/lifecycle'; -export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditingService { +export abstract class AbstractWorkspaceEditingService extends Disposable implements IWorkspaceEditingService { declare readonly _serviceBrand: undefined; @@ -51,7 +52,9 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - ) { } + ) { + super(); + } async pickNewWorkspacePath(): Promise { const availableFileSystems = [Schemas.file]; diff --git a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts index f0453fbdc65c1..a896a9ed6d141 100644 --- a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts @@ -67,10 +67,10 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi } private registerListeners(): void { - this.lifecycleService.onBeforeShutdown(e => { + this._register(this.lifecycleService.onBeforeShutdown(e => { const saveOperation = this.saveUntitledBeforeShutdown(e.reason); e.veto(saveOperation, 'veto.untitledWorkspace'); - }); + })); } private async saveUntitledBeforeShutdown(reason: ShutdownReason): Promise { diff --git a/src/vs/workbench/services/workspaces/test/browser/workspaces.test.ts b/src/vs/workbench/services/workspaces/test/browser/workspaces.test.ts index a1e1a9f728f33..166b616d4c596 100644 --- a/src/vs/workbench/services/workspaces/test/browser/workspaces.test.ts +++ b/src/vs/workbench/services/workspaces/test/browser/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 { getWorkspaceIdentifier, getSingleFolderWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index 6b9bd8a882db0..203cde08ee20b 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/test/browser/codeeditor.test.ts b/src/vs/workbench/test/browser/codeeditor.test.ts index 686bc17e88bec..59ac60e96ffbc 100644 --- a/src/vs/workbench/test/browser/codeeditor.test.ts +++ b/src/vs/workbench/test/browser/codeeditor.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/test/browser/contributions.test.ts b/src/vs/workbench/test/browser/contributions.test.ts index 8f878d6ab538c..adb429673b1c9 100644 --- a/src/vs/workbench/test/browser/contributions.test.ts +++ b/src/vs/workbench/test/browser/contributions.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { isCI } from 'vs/base/common/platform'; diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 38e6ef77ccef6..736ab2610f4d1 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Part } from 'vs/workbench/browser/part'; import { isEmptyObject } from 'vs/base/common/types'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; @@ -115,7 +115,7 @@ suite('Workbench parts', () => { }); teardown(() => { - mainWindow.document.body.removeChild(fixture); + fixture.remove(); disposables.clear(); }); diff --git a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts index 47eb7efceba9b..2b68bdea2ddd6 100644 --- a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { BreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; diff --git a/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts index be64312662062..60d9d1339816f 100644 --- a/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/diffEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/test/browser/parts/editor/editor.test.ts b/src/vs/workbench/test/browser/parts/editor/editor.test.ts index eaf064119d07b..00add777446e7 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorResourceAccessor, SideBySideEditor, EditorInputWithPreferredResource, EditorInputCapabilities, isEditorIdentifier, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, isResourceEditorInput, isUntitledResourceEditorInput, isResourceDiffEditorInput, isEditorInputWithOptionsAndGroup, EditorInputWithOptions, isEditorInputWithOptions, isEditorInput, EditorInputWithOptionsAndGroup, isResourceSideBySideEditorInput, IResourceSideBySideEditorInput, isTextEditorViewState, isResourceMergeEditorInput, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/test/browser/parts/editor/editorCommandsContext.test.ts b/src/vs/workbench/test/browser/parts/editor/editorCommandsContext.test.ts new file mode 100644 index 0000000000000..1cc2f10170823 --- /dev/null +++ b/src/vs/workbench/test/browser/parts/editor/editorCommandsContext.test.ts @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, registerTestFileEditor, registerTestResourceEditor, TestFileEditorInput, createEditorPart, registerTestSideBySideEditor, TestEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { URI } from 'vs/base/common/uri'; +import { resolveCommandsContext } from 'vs/workbench/browser/parts/editor/editorCommandsContext'; +import { IEditorCommandsContext } from 'vs/workbench/common/editor'; + +suite('Resolving Editor Commands Context', () => { + + const disposables = new DisposableStore(); + + const TEST_EDITOR_ID = 'MyTestEditorForEditors'; + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + setup(() => { + instantiationService = workbenchInstantiationService(undefined, disposables); + accessor = instantiationService.createInstance(TestServiceAccessor); + + disposables.add(accessor.untitledTextEditorService); + disposables.add(registerTestFileEditor()); + disposables.add(registerTestSideBySideEditor()); + disposables.add(registerTestResourceEditor()); + disposables.add(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput)])); + }); + + teardown(() => { + disposables.clear(); + }); + + let index = 0; + function input(id = String(index++)): EditorInput { + return disposables.add(new TestEditorInput(URI.parse(`file://${id}`), 'testInput')); + } + + async function createServices(): Promise { + const instantiationService = workbenchInstantiationService(undefined, disposables); + + const part = await createEditorPart(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, part); + + const editorService = disposables.add(instantiationService.createInstance(EditorService, undefined)); + instantiationService.stub(IEditorService, editorService); + + return instantiationService.createInstance(TestServiceAccessor); + } + + test('use editor group selection', async () => { + const accessor = await createServices(); + const activeGroup = accessor.editorGroupService.activeGroup; + const instantiationService = accessor.instantiationService; + + const input1 = input(); + const input2 = input(); + const input3 = input(); + activeGroup.openEditor(input1, { pinned: true }); + activeGroup.openEditor(input2, { pinned: true }); + activeGroup.openEditor(input3, { pinned: true }); + + activeGroup.setSelection(input1, [input2]); + + // use editor commands context + const editorCommandContext: IEditorCommandsContext = { groupId: activeGroup.id, editorIndex: activeGroup.getIndexOfEditor(input1), preserveFocus: true }; + const resolvedContext1 = instantiationService.invokeFunction(resolveCommandsContext, [editorCommandContext]); + + assert.strictEqual(resolvedContext1.groupedEditors.length, 1); + assert.strictEqual(resolvedContext1.groupedEditors[0].group.id, activeGroup.id); + assert.strictEqual(resolvedContext1.groupedEditors[0].editors.length, 2); + assert.strictEqual(resolvedContext1.groupedEditors[0].editors[0], input1); + assert.strictEqual(resolvedContext1.groupedEditors[0].editors[1], input2); + assert.strictEqual(resolvedContext1.preserveFocus, true); + + // use URI + const resolvedContext2 = instantiationService.invokeFunction(resolveCommandsContext, [input2.resource]); + + assert.strictEqual(resolvedContext2.groupedEditors.length, 1); + assert.strictEqual(resolvedContext2.groupedEditors[0].group.id, activeGroup.id); + assert.strictEqual(resolvedContext2.groupedEditors[0].editors.length, 2); + assert.strictEqual(resolvedContext2.groupedEditors[0].editors[0], input2); + assert.strictEqual(resolvedContext2.groupedEditors[0].editors[1], input1); + assert.strictEqual(resolvedContext2.preserveFocus, false); + + // use URI and commandContext + const editor1CommandContext: IEditorCommandsContext = { groupId: activeGroup.id, editorIndex: activeGroup.getIndexOfEditor(input1), preserveFocus: true }; + const resolvedContext3 = instantiationService.invokeFunction(resolveCommandsContext, [input1.resource, editor1CommandContext]); + + assert.strictEqual(resolvedContext3.groupedEditors.length, 1); + assert.strictEqual(resolvedContext3.groupedEditors[0].group.id, activeGroup.id); + assert.strictEqual(resolvedContext3.groupedEditors[0].editors.length, 2); + assert.strictEqual(resolvedContext3.groupedEditors[0].editors[0], input1); + assert.strictEqual(resolvedContext3.groupedEditors[0].editors[1], input2); + assert.strictEqual(resolvedContext3.preserveFocus, true); + }); + + test('don\'t use editor group selection', async () => { + const accessor = await createServices(); + const activeGroup = accessor.editorGroupService.activeGroup; + const instantiationService = accessor.instantiationService; + + const input1 = input(); + const input2 = input(); + const input3 = input(); + activeGroup.openEditor(input1, { pinned: true }); + activeGroup.openEditor(input2, { pinned: true }); + activeGroup.openEditor(input3, { pinned: true }); + + activeGroup.setSelection(input1, [input2]); + + // use editor commands context + const editorCommandContext: IEditorCommandsContext = { groupId: activeGroup.id, editorIndex: activeGroup.getIndexOfEditor(input3), preserveFocus: true }; + const resolvedContext1 = instantiationService.invokeFunction(resolveCommandsContext, [editorCommandContext]); + + assert.strictEqual(resolvedContext1.groupedEditors.length, 1); + assert.strictEqual(resolvedContext1.groupedEditors[0].group.id, activeGroup.id); + assert.strictEqual(resolvedContext1.groupedEditors[0].editors.length, 1); + assert.strictEqual(resolvedContext1.groupedEditors[0].editors[0], input3); + assert.strictEqual(resolvedContext1.preserveFocus, true); + + // use URI + const resolvedContext2 = instantiationService.invokeFunction(resolveCommandsContext, [input3.resource]); + + assert.strictEqual(resolvedContext2.groupedEditors.length, 1); + assert.strictEqual(resolvedContext2.groupedEditors[0].group.id, activeGroup.id); + assert.strictEqual(resolvedContext2.groupedEditors[0].editors.length, 1); + assert.strictEqual(resolvedContext2.groupedEditors[0].editors[0], input3); + assert.strictEqual(resolvedContext2.preserveFocus, false); + }); + + test('inactive edior group command context', async () => { + const accessor = await createServices(); + const editorGroupService = accessor.editorGroupService; + const instantiationService = accessor.instantiationService; + + const group1 = editorGroupService.activeGroup; + const group2 = editorGroupService.addGroup(group1, GroupDirection.RIGHT); + + const input11 = input(); + const input12 = input(); + group1.openEditor(input11, { pinned: true }); + group1.openEditor(input12, { pinned: true }); + + const input21 = input(); + group2.openEditor(input21, { pinned: true }); + + editorGroupService.activateGroup(group1); + group1.setSelection(input11, [input12]); + + // use editor commands context of inactive group with editor index + const editorCommandContext1: IEditorCommandsContext = { groupId: group2.id, editorIndex: group2.getIndexOfEditor(input21), preserveFocus: true }; + const resolvedContext1 = instantiationService.invokeFunction(resolveCommandsContext, [editorCommandContext1]); + + assert.strictEqual(resolvedContext1.groupedEditors.length, 1); + assert.strictEqual(resolvedContext1.groupedEditors[0].group.id, group2.id); + assert.strictEqual(resolvedContext1.groupedEditors[0].editors.length, 1); + assert.strictEqual(resolvedContext1.groupedEditors[0].editors[0], input21); + assert.strictEqual(resolvedContext1.preserveFocus, true); + + // use editor commands context of inactive group without editor index + const editorCommandContext2: IEditorCommandsContext = { groupId: group2.id, preserveFocus: true }; + const resolvedContext2 = instantiationService.invokeFunction(resolveCommandsContext, [editorCommandContext2]); + + assert.strictEqual(resolvedContext2.groupedEditors.length, 1); + assert.strictEqual(resolvedContext2.groupedEditors[0].group.id, group2.id); + assert.strictEqual(resolvedContext2.groupedEditors[0].editors.length, 1); + assert.strictEqual(resolvedContext1.groupedEditors[0].editors[0], input21); + assert.strictEqual(resolvedContext2.preserveFocus, true); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); +}); diff --git a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts index b6ece8430e7e7..a2047327ff1de 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorDiffModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { TextDiffEditorModel } from 'vs/workbench/common/editor/textDiffEditorModel'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts index 22a0fd80cf981..f87bac4abaa59 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorGroupModel, IGroupEditorChangeEvent, IGroupEditorCloseEvent, IGroupEditorMoveEvent, IGroupEditorOpenEvent, ISerializedEditorGroupModel, isGroupEditorChangeEvent, isGroupEditorCloseEvent, isGroupEditorMoveEvent, isGroupEditorOpenEvent } from 'vs/workbench/common/editor/editorGroupModel'; import { EditorExtensions, IEditorFactoryRegistry, IFileEditorInput, IEditorSerializer, CloseDirection, EditorsOrder, IResourceDiffEditorInput, IResourceSideBySideEditorInput, SideBySideEditor, EditorCloseContext, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts index 4c1b74c31e770..e66b98bd3ad22 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorInput.test.ts @@ -3,7 +3,7 @@ * Licensed 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/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index f293125aad4cd..05cc0e521e6a7 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IModelService } from 'vs/editor/common/services/model'; diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index 3d817b86fea87..2aae30b52436b 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { WorkspaceTrustRequiredPlaceholderEditor } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions, EditorInputCapabilities, IEditorDescriptor, IEditorPane } from 'vs/workbench/common/editor'; diff --git a/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts index 80765957797e0..53db7729f74a0 100644 --- a/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/filteredEditorGroupModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { EditorGroupModel, ISerializedEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; import { EditorExtensions, IEditorFactoryRegistry, IFileEditorInput, IEditorSerializer, EditorsOrder, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts index 2002d562f559f..c3dd4fd67a81c 100644 --- a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts index cbd6264885fe5..4be1b5c4fc44e 100644 --- a/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/sideBySideEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed 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/workbench/test/browser/parts/editor/textEditorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts index 60ab56b329ad7..a13dc9884dcd2 100644 --- a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts @@ -3,7 +3,7 @@ * Licensed 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, toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { workbenchInstantiationService, TestServiceAccessor, registerTestFileEditor, createEditorPart, TestTextFileEditor } from 'vs/workbench/test/browser/workbenchTestServices'; diff --git a/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts index 029ad438984c6..24d70f5a76e72 100644 --- a/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textResourceEditorInput.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { TextResourceEditorModel } from 'vs/workbench/common/editor/textResourceEditorModel'; diff --git a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts index 3f0f767be4290..67a0a5140e13f 100644 --- a/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts +++ b/src/vs/workbench/test/browser/parts/statusbar/statusbarModel.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { StatusbarViewModel } from 'vs/workbench/browser/parts/statusbar/statusbarModel'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; diff --git a/src/vs/workbench/test/browser/quickAccess.test.ts b/src/vs/workbench/test/browser/quickAccess.test.ts index 9d71c84ccf332..49c7811127462 100644 --- a/src/vs/workbench/test/browser/quickAccess.test.ts +++ b/src/vs/workbench/test/browser/quickAccess.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions, IQuickAccessProvider, QuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickPick, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; diff --git a/src/vs/workbench/test/browser/viewlet.test.ts b/src/vs/workbench/test/browser/viewlet.test.ts index eefc87cb2d69f..0c970c81ce784 100644 --- a/src/vs/workbench/test/browser/viewlet.test.ts +++ b/src/vs/workbench/test/browser/viewlet.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { Registry } from 'vs/platform/registry/common/platform'; import { PaneCompositeDescriptor, Extensions, PaneCompositeRegistry, PaneComposite } from 'vs/workbench/browser/panecomposite'; import { isFunction } from 'vs/base/common/types'; diff --git a/src/vs/workbench/test/browser/webview.test.ts b/src/vs/workbench/test/browser/webview.test.ts index 3d0c3701f1410..31d546e934f46 100644 --- a/src/vs/workbench/test/browser/webview.test.ts +++ b/src/vs/workbench/test/browser/webview.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { parentOriginHash } from 'vs/base/browser/iframe'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; diff --git a/src/vs/workbench/test/browser/window.test.ts b/src/vs/workbench/test/browser/window.test.ts index f65d50c3aea54..fe3c2ff236a81 100644 --- a/src/vs/workbench/test/browser/window.test.ts +++ b/src/vs/workbench/test/browser/window.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { IRegisteredCodeWindow } from 'vs/base/browser/dom'; import { CodeWindow, mainWindow } from 'vs/base/browser/window'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index bad005e50c6f9..b141508b4e169 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -40,7 +40,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { ITextResourceConfigurationService, ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position'; -import { IMenuService, MenuId, IMenu, IMenuChangeEvent } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, IMenu, IMenuChangeEvent, IMenuActionOptions, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { ContextKeyValue, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, ITextSnapshot } from 'vs/editor/common/model'; @@ -161,7 +161,7 @@ import { IUserDataProfile, IUserDataProfilesService, toUserDataProfile, UserData import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { EnablementState, IResourceExtension, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ILocalExtension, IGalleryExtension, InstallOptions, IExtensionIdentifier, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant, Metadata, InstallExtensionResult, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant, Metadata, InstallExtensionResult, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Codicon } from 'vs/base/common/codicons'; import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; import { IRemoteSocketFactoryService, RemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; @@ -556,6 +556,14 @@ export class TestMenuService implements IMenuService { }; } + getMenuActions(id: MenuId, contextKeyService: IContextKeyService, options?: IMenuActionOptions): [string, Array][] { + throw new Error('Method not implemented.'); + } + + getMenuContexts(id: MenuId): ReadonlySet { + throw new Error('Method not implemented.'); + } + resetHiddenStates(): void { // nothing } @@ -1364,7 +1372,7 @@ export class TestLifecycleService extends Disposable implements ILifecycleServic this._onWillShutdown.fire({ join: p => { - this.shutdownJoiners.push(p); + this.shutdownJoiners.push(typeof p === 'function' ? p() : p); }, joiners: () => [], force: () => { /* No-Op in tests */ }, @@ -1405,8 +1413,8 @@ export class TestWillShutdownEvent implements WillShutdownEvent { reason = ShutdownReason.CLOSE; token = CancellationToken.None; - join(promise: Promise, joiner: IWillShutdownEventJoiner): void { - this.value.push(promise); + join(promise: Promise | (() => Promise), joiner: IWillShutdownEventJoiner): void { + this.value.push(typeof promise === 'function' ? promise() : promise); } force() { /* No-Op in tests */ } @@ -2117,13 +2125,13 @@ export class TestRemoteAgentService implements IRemoteAgentService { async logTelemetry(eventName: string, data?: ITelemetryData): Promise { } async flushTelemetry(): Promise { } async getRoundTripTime(): Promise { return undefined; } + async endConnection(): Promise { } } export class TestRemoteExtensionsScannerService implements IRemoteExtensionsScannerService { declare readonly _serviceBrand: undefined; async whenExtensionsReady(): Promise { } scanExtensions(): Promise { throw new Error('Method not implemented.'); } - scanSingleExtension(): Promise { throw new Error('Method not implemented.'); } } export class TestWorkbenchExtensionEnablementService implements IWorkbenchExtensionEnablementService { @@ -2167,9 +2175,6 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens zip(extension: ILocalExtension): Promise { throw new Error('Method not implemented.'); } - unzip(zipLocation: URI): Promise { - throw new Error('Method not implemented.'); - } getManifest(vsix: URI): Promise> { throw new Error('Method not implemented.'); } @@ -2205,6 +2210,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens getInstalledWorkspaceExtensions(): Promise { throw new Error('Method not implemented.'); } installResourceExtension(): Promise { throw new Error('Method not implemented.'); } getExtensions(): Promise { throw new Error('Method not implemented.'); } + resetPinnedStateForAllUserExtensions(pinned: boolean): Promise { throw new Error('Method not implemented.'); } } export class TestUserDataProfileService implements IUserDataProfileService { diff --git a/src/vs/workbench/test/common/memento.test.ts b/src/vs/workbench/test/common/memento.test.ts index 84502b1c32185..7850379177f00 100644 --- a/src/vs/workbench/test/common/memento.test.ts +++ b/src/vs/workbench/test/common/memento.test.ts @@ -3,7 +3,7 @@ * Licensed 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 { StorageScope, IStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index de27e6b87cd1c..ac087cde92926 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { NotificationsModel, NotificationViewItem, INotificationChangeEvent, NotificationChangeType, NotificationViewItemContentChangeKind, IStatusMessageChangeEvent, StatusMessageChangeType, INotificationsFilter } from 'vs/workbench/common/notifications'; import { Action } from 'vs/base/common/actions'; import { INotification, Severity, NotificationsFilter, NotificationPriority } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/workbench/test/common/resources.test.ts b/src/vs/workbench/test/common/resources.test.ts index 6ce08bcf9ea27..82cb89a37a366 100644 --- a/src/vs/workbench/test/common/resources.test.ts +++ b/src/vs/workbench/test/common/resources.test.ts @@ -3,7 +3,7 @@ * Licensed 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/workbench/test/common/utils.ts b/src/vs/workbench/test/common/utils.ts index b28eb1d1c8bea..697812f153cf3 100644 --- a/src/vs/workbench/test/common/utils.ts +++ b/src/vs/workbench/test/common/utils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; +import assert from 'assert'; import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry'; /** diff --git a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts b/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts index 14536883beabe..ff49db5e3c7d2 100644 --- a/src/vs/workbench/test/electron-sandbox/resolveExternal.test.ts +++ b/src/vs/workbench/test/electron-sandbox/resolveExternal.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 { NativeWindow } from 'vs/workbench/electron-sandbox/window'; import { ITunnelService, RemoteTunnel } from 'vs/platform/tunnel/common/tunnel'; diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 45f6d6d98f43f..e130b0837f28f 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -48,6 +48,7 @@ import { NativeWorkingCopyBackupService } from 'vs/workbench/services/workingCop import { CancellationToken } from 'vs/base/common/cancellation'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { AuthInfo, Credentials } from 'vs/platform/request/common/request'; export class TestSharedProcessService implements ISharedProcessService { @@ -124,7 +125,7 @@ export class TestNativeHostService implements INativeHostService { async getProcessId(): Promise { throw new Error('Method not implemented.'); } async killProcess(): Promise { } async setDocumentEdited(edited: boolean): Promise { } - async openExternal(url: string): Promise { return false; } + async openExternal(url: string, defaultApplication?: string): Promise { return false; } async updateTouchBar(): Promise { } async moveItemToTrash(): Promise { } async newWindowTab(): Promise { } @@ -144,6 +145,7 @@ export class TestNativeHostService implements INativeHostService { async openDevTools(options?: Partial & INativeHostOptions | undefined): Promise { } async toggleDevTools(): Promise { } async resolveProxy(url: string): Promise { return undefined; } + async lookupAuthorization(authInfo: AuthInfo): Promise { return undefined; } async loadCertificates(): Promise { return []; } async findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise { return -1; } async readClipboardText(type?: 'selection' | 'clipboard' | undefined): Promise { return ''; } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index a4e3fa69d6ec5..8ec34fb9b6c46 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -193,6 +193,9 @@ import 'vs/workbench/contrib/inlineChat/browser/inlineChat.contribution'; // Interactive import 'vs/workbench/contrib/interactive/browser/interactive.contribution'; +// repl +import 'vs/workbench/contrib/replNotebook/browser/repl.contribution'; + // Testing import 'vs/workbench/contrib/testing/browser/testing.contribution'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index c4b6e9c27d032..d4db1d498963f 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -118,6 +118,9 @@ import 'vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution // Issues import 'vs/workbench/contrib/issue/electron-sandbox/issue.contribution'; +// Process +import 'vs/workbench/contrib/issue/electron-sandbox/process.contribution'; + // Remote import 'vs/workbench/contrib/remote/electron-sandbox/remote.contribution'; diff --git a/src/vscode-dts/README.md b/src/vscode-dts/README.md index 9b3640d920887..7d8c057c4808b 100644 --- a/src/vscode-dts/README.md +++ b/src/vscode-dts/README.md @@ -14,7 +14,7 @@ This is the place for the stable API and for API proposals. ## Add a new proposal 1. create a _new_ file in this directory, its name must follow this pattern `vscode.proposed.[a-zA-Z]+.d.ts` -1. creating the proposal-file will automatically update `src/vs/workbench/services/extensions/common/extensionsApiProposals.ts` (make sure to run `yarn watch`) +1. creating the proposal-file will automatically update `src/vs/platform/extensions/common/extensionsApiProposals.ts` (make sure to run `yarn watch`) 1. declare and implement your proposal 1. make sure to use the `checkProposedApiEnabled` and/or `isProposedApiEnabled`-utils to enforce the API being proposed. Make sure to invoke them with your proposal's name which got generated into `extensionsApiProposals.ts` 1. Most likely will need to add your proposed api to vscode-api-tests as well diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 50e8060b8ee18..8c22232e2b6e5 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -535,7 +535,7 @@ declare module 'vscode' { /** * Represents sources that can cause {@link window.onDidChangeTextEditorSelection selection change events}. - */ + */ export enum TextEditorSelectionChangeKind { /** * Selection changed due to typing in the editor. @@ -5186,7 +5186,7 @@ declare module 'vscode' { /** * Creates a new list of inline completion items. - */ + */ constructor(items: InlineCompletionItem[]); } @@ -6365,7 +6365,7 @@ declare module 'vscode' { export enum ConfigurationTarget { /** * Global configuration - */ + */ Global = 1, /** @@ -11116,8 +11116,8 @@ declare module 'vscode' { canSelectMany?: boolean; /** - * An optional interface to implement drag and drop in the tree view. - */ + * An optional interface to implement drag and drop in the tree view. + */ dragAndDropController?: TreeDragAndDropController; /** @@ -11378,8 +11378,8 @@ declare module 'vscode' { */ export interface TreeCheckboxChangeEvent { /** - * The items that were checked or unchecked. - */ + * The items that were checked or unchecked. + */ readonly items: ReadonlyArray<[T, TreeItemCheckboxState]>; } @@ -11419,8 +11419,8 @@ declare module 'vscode' { readonly onDidChangeVisibility: Event; /** - * An event to signal that an element or root has either been checked or unchecked. - */ + * An event to signal that an element or root has either been checked or unchecked. + */ readonly onDidChangeCheckboxState: Event>; /** @@ -11697,8 +11697,8 @@ declare module 'vscode' { } /** - * Checkbox state of the tree item - */ + * Checkbox state of the tree item + */ export enum TreeItemCheckboxState { /** * Determines an item is unchecked @@ -11787,8 +11787,8 @@ declare module 'vscode' { color?: ThemeColor; /** - * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. - */ + * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. + */ location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; /** @@ -12668,7 +12668,7 @@ declare module 'vscode' { /** * The reason why the document was changed. * Is `undefined` if the reason is not known. - */ + */ readonly reason: TextDocumentChangeReason | undefined; } @@ -15374,7 +15374,7 @@ declare module 'vscode' { * * @param rendererId The renderer ID to communicate with * @returns A new notebook renderer messaging object. - */ + */ export function createRendererMessaging(rendererId: string): NotebookRendererMessaging; } @@ -16183,6 +16183,13 @@ declare module 'vscode' { * When true, the debug viewlet will not be automatically revealed for this session. */ suppressDebugView?: boolean; + + /** + * Signals to the editor that the debug session was started from a test run + * request. This is used to link the lifecycle of the debug session and + * test run in UI actions. + */ + testRun?: TestRun; } /** @@ -16369,7 +16376,7 @@ declare module 'vscode' { /** * Add breakpoints. * @param breakpoints The breakpoints to add. - */ + */ export function addBreakpoints(breakpoints: readonly Breakpoint[]): void; /** @@ -16907,17 +16914,17 @@ declare module 'vscode' { /** * Whether it is possible to be signed into multiple accounts at once with this provider. * If not specified, will default to false. - */ + */ readonly supportsMultipleAccounts?: boolean; } /** - * An {@link Event} which fires when an {@link AuthenticationSession} is added, removed, or changed. - */ + * An {@link Event} which fires when an {@link AuthenticationSession} is added, removed, or changed. + */ export interface AuthenticationProviderAuthenticationSessionsChangeEvent { /** * The {@link AuthenticationSession AuthenticationSessions} of the {@link AuthenticationProvider} that have been added. - */ + */ readonly added: readonly AuthenticationSession[] | undefined; /** @@ -17144,7 +17151,7 @@ declare module 'vscode' { * @param id Identifier for the controller, must be globally unique. * @param label A human-readable label for the controller. * @returns An instance of the {@link TestController}. - */ + */ export function createTestController(id: string, label: string): TestController; } @@ -18995,7 +19002,7 @@ declare module 'vscode' { * Represents a language model response. * * @see {@link LanguageModelAccess.chatRequest} - */ + */ export interface LanguageModelChatResponse { /** diff --git a/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts b/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts index 63000738c0f3d..b717159885b5b 100644 --- a/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts +++ b/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts @@ -6,23 +6,41 @@ declare module 'vscode' { export class FileCoverage2 extends FileCoverage { /** - * Test {@link TestItem} this file coverage is generated from. If undefined, - * the editor will assume the coverage is the overall summary coverage for - * the entire file. - * - * If per-test coverage is available, an extension should append multiple - * `FileCoverage` instances with this property set for each test item. It - * must also append a `FileCoverage` instance without this property set to - * represent the overall coverage of the file. + * A list of {@link TestItem test cases} that generated coverage in this + * file. If set, then {@link TestRunProfile.loadDetailedCoverageForTest} + * should also be defined in order to retrieve detailed coverage information. */ - testItem?: TestItem; + fromTests: TestItem[]; constructor( uri: Uri, statementCoverage: TestCoverageCount, branchCoverage?: TestCoverageCount, declarationCoverage?: TestCoverageCount, - testItem?: TestItem, + fromTests?: TestItem[], ); } + + export interface TestRunProfile { + /** + * An extension-provided function that provides detailed statement and + * function-level coverage for a single test in a file. This is the per-test + * sibling of {@link TestRunProfile.loadDetailedCoverage}, called only if + * a test item is provided in {@link FileCoverage.fromTests} and only for + * files where such data is reported. + * + * The editor will call this when user asks to view coverage for a test in + * a file, and the returned coverage information is used to display exactly + * what code was run by that test. + * + * The {@link FileCoverage} object passed to this function is the same + * instance emitted on {@link TestRun.addCoverage} calls associated with this profile. + * + * @param testRun The test run that generated the coverage data. + * @param fileCoverage The file coverage object to load detailed coverage for. + * @param fromTestItem The test item to request coverage information for. + * @param token A cancellation token that indicates the operation should be cancelled. + */ + loadDetailedCoverageForTest?: (testRun: TestRun, fileCoverage: FileCoverage, fromTestItem: TestItem, token: CancellationToken) => Thenable; + } } diff --git a/src/vscode-dts/vscode.proposed.authGetSessions.d.ts b/src/vscode-dts/vscode.proposed.authGetSessions.d.ts index 20a692c018058..6d425ed3232b3 100644 --- a/src/vscode-dts/vscode.proposed.authGetSessions.d.ts +++ b/src/vscode-dts/vscode.proposed.authGetSessions.d.ts @@ -7,42 +7,50 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/152399 - export interface AuthenticationForceNewSessionOptions { - /** - * The session that you are asking to be recreated. The Auth Provider can use this to - * help guide the user to log in to the correct account. - */ - sessionToRecreate?: AuthenticationSession; - } + // FOR THE CONSUMER export namespace authentication { /** - * Get all authentication sessions matching the desired scopes that this extension has access to. In order to request access, - * use {@link getSession}. To request an additional account, specify {@link AuthenticationGetSessionOptions.clearSessionPreference} - * and {@link AuthenticationGetSessionOptions.createIfNone} together. + * Get all accounts that the user is logged in to for the specified provider. + * Use this paired with {@link getSession} in order to get an authentication session for a specific account. * * Currently, there are only two authentication providers that are contributed from built in extensions * to the editor that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. * - * @param providerId The id of the provider to use - * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider - * @returns A thenable that resolves to a readonly array of authentication sessions. + * Note: Getting accounts does not imply that your extension has access to that account or its authentication sessions. You can verify access to the account by calling {@link getSession}. + * + * @param providerId The id of the provider to use + * @returns A thenable that resolves to a readonly array of authentication accounts. + */ + export function getAccounts(providerId: string): Thenable; + } + + export interface AuthenticationGetSessionOptions { + /** + * The account that you would like to get a session for. This is passed down to the Authentication Provider to be used for creating the correct session. */ - export function getSessions(providerId: string, scopes: readonly string[]): Thenable; + account?: AuthenticationSessionAccountInformation; } - /** - * The options passed in to the provider when creating a session. - */ - export interface AuthenticationProviderCreateSessionOptions { + // FOR THE AUTH PROVIDER + + export interface AuthenticationProviderSessionOptions { /** - * The session that is being asked to be recreated. If this is passed in, the provider should - * attempt to recreate the session based on the information in this session. + * 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. */ - sessionToRecreate?: AuthenticationSession; + account?: AuthenticationSessionAccountInformation; } export interface AuthenticationProvider { + /** + * Get a list of 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: readonly string[] | undefined, options: AuthenticationProviderSessionOptions): Thenable; /** * Prompts a user to login. * @@ -57,6 +65,6 @@ declare module 'vscode' { * @param options Additional options for creating a session. * @returns A promise that resolves to an authentication session. */ - createSession(scopes: readonly string[], options: AuthenticationProviderCreateSessionOptions): Thenable; + createSession(scopes: readonly string[], options: AuthenticationProviderSessionOptions): Thenable; } } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index cd2ec7ba91910..ab43814c4f487 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -59,7 +59,8 @@ declare module 'vscode' { title: string; message: string; data: any; - constructor(title: string, message: string, data: any); + buttons?: string[]; + constructor(title: string, message: string, data: any, buttons?: string[]); } export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseConfirmationPart; @@ -102,7 +103,7 @@ declare module 'vscode' { * TODO@API should this be MarkdownString? * TODO@API should actually be a more generic function that takes an array of buttons */ - confirmation(title: string, message: string, data: any): void; + confirmation(title: string, message: string, data: any, buttons?: string[]): void; /** * Push a warning to this stream. Short-hand for diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 4e328978a9dcd..ce699b1fec9c8 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// version: 2 + declare module 'vscode' { /** @@ -27,6 +29,22 @@ declare module 'vscode' { Editor = 4 } + export class ChatRequestEditorData { + //TODO@API should be the editor + document: TextDocument; + selection: Selection; + wholeRange: Range; + + constructor(document: TextDocument, selection: Selection, wholeRange: Range); + } + + export class ChatRequestNotebookData { + //TODO@API should be the editor + readonly cell: TextDocument; + + constructor(cell: TextDocument); + } + export interface ChatRequest { /** * The attempt number of the request. The first request has attempt number 0. @@ -40,8 +58,16 @@ declare module 'vscode' { /** * The location at which the chat is happening. This will always be one of the supported values + * + * @deprecated */ readonly location: ChatLocation; + + /** + * Information that is specific to the location at which chat is happening, e.g within a document, notebook, + * or terminal. Will be `undefined` for the chat panel. + */ + readonly location2: ChatRequestEditorData | ChatRequestNotebookData | undefined; } export interface ChatParticipant { diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 7fc7c3e3b97b4..9abbdc5d1aa25 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -10,20 +10,29 @@ declare module 'vscode' { part: string; } + export interface ChatResponseFragment2 { + index: number; + part: LanguageModelChatResponseTextPart | LanguageModelChatResponseFunctionUsePart; + } + // @API extension ship a d.ts files for their options /** * Represents a large language model that accepts ChatML messages and produces a streaming response */ - export interface ChatResponseProvider { + export interface LanguageModelChatProvider { onDidReceiveLanguageModelResponse2?: Event<{ readonly extensionId: string; readonly participant?: string; readonly tokenCount?: number }>; provideLanguageModelResponse(messages: LanguageModelChatMessage[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; + provideLanguageModelResponse2?(messages: LanguageModelChatMessage[], options: LanguageModelChatRequestOptions, extensionId: string, progress: Progress, token: CancellationToken): Thenable; + provideTokenCount(text: string | LanguageModelChatMessage, token: CancellationToken): Thenable; } + export type ChatResponseProvider = LanguageModelChatProvider; + export interface ChatResponseProviderMetadata { readonly vendor: string; @@ -64,14 +73,14 @@ declare module 'vscode' { export namespace chat { /** - * Register a LLM as chat response provider to the editor. - * - * - * @param id - * @param provider - * @param metadata - */ + * @deprecated use `lm.registerChatResponseProvider` instead + */ export function registerChatResponseProvider(id: string, provider: ChatResponseProvider, metadata: ChatResponseProviderMetadata): Disposable; } + export namespace lm { + + export function registerChatModelProvider(id: string, provider: LanguageModelChatProvider, metadata: ChatResponseProviderMetadata): Disposable; + } + } diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts index 1b404980e2ccc..62f07234bb1af 100644 --- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts +++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts @@ -19,15 +19,6 @@ declare module 'vscode' { * @param icon An icon to display when selecting context in the picker UI. */ export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver, fullName?: string, icon?: ThemeIcon): Disposable; - - /** - * Attaches a chat context with the specified name, value, and location. - * - * @param name - The name of the chat context. - * @param value - The value of the chat context. - * @param location - The location of the chat context. - */ - export function attachContext(name: string, value: string | Uri | Location | unknown, location: ChatLocation.Panel): void; } export interface ChatVariableValue { diff --git a/src/vscode-dts/vscode.proposed.commentReveal.d.ts b/src/vscode-dts/vscode.proposed.commentReveal.d.ts new file mode 100644 index 0000000000000..168c4691de5de --- /dev/null +++ b/src/vscode-dts/vscode.proposed.commentReveal.d.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // @alexr00 https://github.com/microsoft/vscode/issues/167253 + + /** + * Options to reveal a comment thread in an editor. + */ + export interface CommentThreadRevealOptions { + /** + * By default, the comment thread will be focused. Set `preserveFocus` to `true` to maintain the original focus. + */ + preserveFocus?: boolean; + + /** + * Focus the comment thread reply editor, if the thread supports replying. + */ + focusReply?: boolean; + } + + export interface CommentThread { + /** + * Reveal the comment thread in an editor. + */ + reveal(options?: CommentThreadRevealOptions): Thenable; + } + +} diff --git a/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts index e09f5a34d6bc2..547ee182227ea 100644 --- a/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts +++ b/src/vscode-dts/vscode.proposed.commentThreadApplicability.d.ts @@ -28,5 +28,15 @@ declare module 'vscode' { * Worth noting that we already have this problem for the `comments` property. */ state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability }; + readonly uri: Uri; + range: Range | undefined; + comments: readonly Comment[]; + collapsibleState: CommentThreadCollapsibleState; + canReply: boolean; + contextValue?: string; + label?: string; + dispose(): void; + // Part of the comment reveal proposal + reveal(options?: CommentThreadRevealOptions): Thenable; } } diff --git a/src/vs/workbench/workbench.desktop.main.nls.js b/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemChangesMenu.d.ts similarity index 70% rename from src/vs/workbench/workbench.desktop.main.nls.js rename to src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemChangesMenu.d.ts index d6a8b487eaf92..a9d758fa24b0b 100644 --- a/src/vs/workbench/workbench.desktop.main.nls.js +++ b/src/vscode-dts/vscode.proposed.contribSourceControlHistoryItemChangesMenu.d.ts @@ -3,6 +3,5 @@ * 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 +// empty placeholder declaration for the `scm/historyItemChanges/title`-menu contribution point +// https://github.com/microsoft/vscode/issues/201997 diff --git a/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts b/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts index 8ef9b90a8ea0b..d138471f2aff7 100644 --- a/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts +++ b/src/vscode-dts/vscode.proposed.createFileSystemWatcher.d.ts @@ -28,7 +28,7 @@ declare module 'vscode' { * An optional set of glob patterns to exclude from watching. * Glob patterns are always matched relative to the watched folder. */ - readonly excludes?: string[]; + readonly excludes: string[]; } export namespace workspace { diff --git a/src/vscode-dts/vscode.proposed.fileComments.d.ts b/src/vscode-dts/vscode.proposed.fileComments.d.ts index 7370f22f762f4..96e9b181bc641 100644 --- a/src/vscode-dts/vscode.proposed.fileComments.d.ts +++ b/src/vscode-dts/vscode.proposed.fileComments.d.ts @@ -6,68 +6,27 @@ declare module 'vscode' { export interface CommentThread2 { - /** - * The uri of the document the thread has been created on. - */ - readonly uri: Uri; - /** * The range the comment thread is located within the document. The thread icon will be shown - * at the last line of the range. + * at the last line of the range. When set to undefined, the comment will be associated with the + * file, and not a specific range. */ range: Range | undefined; + } + /** + * The ranges a CommentingRangeProvider enables commenting on. + */ + export interface CommentingRanges { /** - * The ordered comments of the thread. - */ - comments: readonly Comment[]; - - /** - * Whether the thread should be collapsed or expanded when opening the document. - * Defaults to Collapsed. - */ - collapsibleState: CommentThreadCollapsibleState; - - /** - * Whether the thread supports reply. - * Defaults to true. - */ - canReply: boolean; - - /** - * Context value of the comment thread. This can be used to contribute thread specific actions. - * For example, a comment thread is given a context value as `editable`. When contributing actions to `comments/commentThread/title` - * using `menus` extension point, you can specify context value for key `commentThread` in `when` expression like `commentThread == editable`. - * ```json - * "contributes": { - * "menus": { - * "comments/commentThread/title": [ - * { - * "command": "extension.deleteCommentThread", - * "when": "commentThread == editable" - * } - * ] - * } - * } - * ``` - * This will show action `extension.deleteCommentThread` only for comment threads with `contextValue` is `editable`. - */ - contextValue?: string; - - /** - * The optional human-readable label describing the {@link CommentThread Comment Thread} + * Enables comments to be added to a file without a specific range. */ - label?: string; - - // from the commentThreadRelevance proposal - state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability }; + enableFileComments: boolean; /** - * Dispose this comment thread. - * - * Once disposed, this comment thread will be removed from visible editors and Comment Panel when appropriate. + * The ranges which allow new comment threads creation. */ - dispose(): void; + ranges?: Range[]; } export interface CommentController { @@ -78,6 +37,6 @@ declare module 'vscode' { /** * Provide a list of ranges which allow new comment threads creation or null for a given document */ - provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; + provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult; } } diff --git a/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts b/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts index bf7bc5ecba37f..8dcfd99852b5a 100644 --- a/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts @@ -13,6 +13,12 @@ declare module 'vscode' { export interface FileSearchQuery { /** * The search pattern to match against file paths. + * To be correctly interpreted by Quick Open, this is interpreted in a relaxed way. The picker will apply its own highlighting and scoring on the results. + * + * Tips for matching in Quick Open: + * With the pattern, the picker will use the file name and file paths to score each entry. The score will determine the ordering and filtering. + * The scoring prioritizes prefix and substring matching. Then, it checks and it checks whether the pattern's letters appear in the same order as in the target (file name and path). + * If a file does not match at all using our criteria, it will be omitted from Quick Open. */ pattern: string; } diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index 2715014a0a8f5..eccc51b53808c 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -60,6 +60,12 @@ declare module 'vscode' { */ // eslint-disable-next-line local/vscode-dts-provider-naming handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; + + provideInlineEdits?(document: TextDocument, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + } + + export interface InlineCompletionContext { + readonly userPrompt?: string; } export interface PartialAcceptInfo { diff --git a/src/vscode-dts/vscode.proposed.lmTools.d.ts b/src/vscode-dts/vscode.proposed.lmTools.d.ts new file mode 100644 index 0000000000000..facbeb20c4dd2 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.lmTools.d.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// version: 2 +// https://github.com/microsoft/vscode/issues/213274 + +declare module 'vscode' { + + // TODO@API capabilities + + export type JSONSchema = object; + + // API -> LM: an tool/function that is available to the language model + export interface LanguageModelChatFunction { + name: string; + description: string; + parametersSchema?: JSONSchema; + } + + // API -> LM: add tools as request option + export interface LanguageModelChatRequestOptions { + // TODO@API this will a heterogeneous array of different types of tools + tools?: LanguageModelChatFunction[]; + } + + // LM -> USER: function that should be used + export class LanguageModelChatResponseFunctionUsePart { + name: string; + parameters: any; + + constructor(name: string, parameters: any); + } + + // LM -> USER: text chunk + export class LanguageModelChatResponseTextPart { + value: string; + + constructor(value: string); + } + + export interface LanguageModelChatResponse { + + stream: AsyncIterable; + } + + + // USER -> LM: the result of a function call + export class LanguageModelChatMessageFunctionResultPart { + name: string; + content: string; + isError: boolean; + + constructor(name: string, content: string, isError?: boolean); + } + + export interface LanguageModelChatMessage { + content2: string | LanguageModelChatMessageFunctionResultPart; + } + + // Tool registration/invoking between extensions + + export namespace lm { + /** + * Register a LanguageModelTool. The tool must also be registered in the package.json `languageModelTools` contribution point. + */ + export function registerTool(name: string, tool: LanguageModelTool): Disposable; + + /** + * A list of all available tools. + */ + export const tools: ReadonlyArray; + + /** + * Invoke a tool with the given parameters. + */ + export function invokeTool(name: string, parameters: Object, token: CancellationToken): Thenable; + } + + // Is the same as LanguageModelChatFunction now, but could have more details in the future + export interface LanguageModelToolDescription { + name: string; + description: string; + parametersSchema?: JSONSchema; + } + + export interface LanguageModelTool { + invoke(parameters: any, token: CancellationToken): Thenable; + } +} diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index 653f6a2d50d1f..6ed185785d86e 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -23,7 +23,7 @@ declare module 'vscode' { * Provide mapped edits for a given document. * @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/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index e7e7ca1c85bbe..9d3151733aa99 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -25,6 +25,7 @@ declare module 'vscode' { // onDidChangeHistoryItemGroups: Event; provideHistoryItems(historyItemGroupId: string, options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; + provideHistoryItems2(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; provideHistoryItemSummary?(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; @@ -34,17 +35,14 @@ declare module 'vscode' { export interface SourceControlHistoryOptions { readonly cursor?: string; readonly limit?: number | { id?: string }; + readonly historyItemGroupIds?: readonly string[]; } export interface SourceControlHistoryItemGroup { readonly id: string; readonly name: string; - readonly base?: Omit; - } - - export interface SourceControlRemoteHistoryItemGroup { - readonly id: string; - readonly name: string; + readonly base?: Omit, 'remote'>; + readonly remote?: Omit, 'remote'>; } export interface SourceControlHistoryItemStatistics { @@ -53,6 +51,11 @@ declare module 'vscode' { readonly deletions: number; } + export interface SourceControlHistoryItemLabel { + readonly title: string; + readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + } + export interface SourceControlHistoryItem { readonly id: string; readonly parentIds: string[]; @@ -61,6 +64,7 @@ declare module 'vscode' { readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; readonly timestamp?: number; readonly statistics?: SourceControlHistoryItemStatistics; + readonly labels?: SourceControlHistoryItemLabel[]; } export interface SourceControlHistoryItemChange { diff --git a/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts b/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts index 9ea09344b8aae..8e44e8e48d68b 100644 --- a/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalShellIntegration.d.ts @@ -29,9 +29,9 @@ declare module 'vscode' { * }); * function summarizeCommandLine(commandLine: TerminalShellExecutionCommandLine) { * return [ - * ` Command line: ${command.ommandLine.value}`, - * ` Confidence: ${command.ommandLine.confidence}`, - * ` Trusted: ${command.ommandLine.isTrusted} + * ` Command line: ${command.commandLine.value}`, + * ` Confidence: ${command.commandLine.confidence}`, + * ` Trusted: ${command.commandLine.isTrusted} * ].join('\n'); * } */ @@ -125,7 +125,7 @@ declare module 'vscode' { /** * An object that contains [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration)-powered * features for the terminal. This will always be `undefined` immediately after the terminal - * is created. Listen to {@link window.onDidActivateTerminalShellIntegration} to be notified + * is created. Listen to {@link window.onDidChangeTerminalShellIntegration} to be notified * when shell integration is activated for a terminal. * * Note that this object may remain undefined if shell integation never activates. For @@ -155,11 +155,13 @@ declare module 'vscode' { * @example * // Execute a command in a terminal immediately after being created * const myTerm = window.createTerminal(); - * window.onDidActivateTerminalShellIntegration(async ({ terminal, shellIntegration }) => { + * window.onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration }) => { * if (terminal === myTerm) { - * const command = shellIntegration.executeCommand('echo "Hello world"'); - * const code = await command.exitCode; - * console.log(`Command exited with code ${code}`); + * const execution = shellIntegration.executeCommand('echo "Hello world"'); + * window.onDidEndTerminalShellExecution(event => { + * if (event.execution === execution) { + * console.log(`Command exited with code ${event.exitCode}`); + * } * } * })); * // Fallback to sendText if there is no shell integration within 3 seconds of launching @@ -175,9 +177,11 @@ declare module 'vscode' { * // Send command to terminal that has been alive for a while * const commandLine = 'echo "Hello world"'; * if (term.shellIntegration) { - * const command = term.shellIntegration.executeCommand({ commandLine }); - * const code = await command.exitCode; - * console.log(`Command exited with code ${code}`); + * const execution = shellIntegration.executeCommand({ commandLine }); + * window.onDidEndTerminalShellExecution(event => { + * if (event.execution === execution) { + * console.log(`Command exited with code ${event.exitCode}`); + * } * } else { * term.sendText(commandLine); * // Without shell integration, we can't know when the command has finished or what the @@ -284,8 +288,29 @@ declare module 'vscode' { readonly execution: TerminalShellExecution; /** - * The exit code reported by the shell. `undefined` means the shell did not report an exit - * code or the shell reported a command started before the command finished. + * The exit code reported by the shell. + * + * Note that `undefined` means the shell either did not report an exit code (ie. the shell + * integration script is misbehaving) or the shell reported a command started before the command + * finished (eg. a sub-shell was opened). Generally this should not happen, depending on the use + * case, it may be best to treat this as a failure. + * + * @example + * const execution = shellIntegration.executeCommand({ + * command: 'echo', + * args: ['Hello world'] + * }); + * window.onDidEndTerminalShellExecution(event => { + * if (event.execution === execution) { + * if (event.exitCode === undefined) { + * console.log('Command finished but exit code is unknown'); + * } else if (event.exitCode === 0) { + * console.log('Command succeeded'); + * } else { + * console.log('Command failed'); + * } + * } + * }); */ readonly exitCode: number | undefined; } diff --git a/src/vscode-dts/vscode.proposed.testMessageStackTrace.d.ts b/src/vscode-dts/vscode.proposed.testMessageStackTrace.d.ts new file mode 100644 index 0000000000000..b3f09b92835ae --- /dev/null +++ b/src/vscode-dts/vscode.proposed.testMessageStackTrace.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. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + export class TestMessage2 extends TestMessage { + /** + * The stack trace associated with the message or failure. + */ + stackTrace?: TestMessageStackFrame[]; + } + + export class TestMessageStackFrame { + /** + * The location of this stack frame. This should be provided as a URI if the + * location of the call frame can be accessed by the editor. + */ + file?: Uri; + + /** + * Position of the stack frame within the file. + */ + position?: Position; + + /** + * The name of the stack frame, typically a method or function name. + */ + label: string; + + /** + * @param label The name of the stack frame + * @param file The file URI of the stack frame + * @param position The position of the stack frame within the file + */ + constructor(label: string, file?: Uri, position?: Position); + } +} diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts index 7223c49a13b2c..0a98250767b15 100644 --- a/test/automation/src/playwrightBrowser.ts +++ b/test/automation/src/playwrightBrowser.ts @@ -72,10 +72,11 @@ async function launchServer(options: LaunchOptions) { logger.log(`Storing log files into '${serverLogsPath}'`); logger.log(`Command line: '${serverLocation}' ${args.join(' ')}`); + const shell: boolean = (process.platform === 'win32'); const serverProcess = spawn( serverLocation, args, - { env } + { env, shell } ); logger.log(`Started server for browser smoke tests (pid: ${serverProcess.pid})`); diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 5e77d55e6bf01..0aeb50e219452 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -231,7 +231,7 @@ export class PlaywrightDriver { return this.page.evaluate(([driver]) => driver.getLogs(), [await this.getDriverHandle()] as const); } - private async evaluateWithDriver(pageFunction: PageFunction[], T>) { + private async evaluateWithDriver(pageFunction: PageFunction) { return this.page.evaluate(pageFunction, [await this.getDriverHandle()]); } diff --git a/test/automation/src/search.ts b/test/automation/src/search.ts index 567ec20fa915d..5cf6018b72a1f 100644 --- a/test/automation/src/search.ts +++ b/test/automation/src/search.ts @@ -61,6 +61,21 @@ export class Search extends Viewlet { await this.submitSearch(); } + async hasActivityBarMoved() { + await this.code.waitForElement('.activitybar'); + + const elementBoundingBox = await this.code.driver.getElementXY('.activitybar'); + return elementBoundingBox !== null && elementBoundingBox.x === 48 && elementBoundingBox.y === 375; + } + + async waitForPageUp(): Promise { + await this.code.dispatchKeybinding('PageUp'); + } + + async waitForPageDown(): Promise { + await this.code.dispatchKeybinding('PageDown'); + } + async submitSearch(): Promise { await this.waitForInputFocus(INPUT); diff --git a/test/automation/src/terminal.ts b/test/automation/src/terminal.ts index b40606928f8c6..8ebcbd686361b 100644 --- a/test/automation/src/terminal.ts +++ b/test/automation/src/terminal.ts @@ -119,6 +119,7 @@ export class Terminal { // Reset await this.code.dispatchKeybinding('Backspace'); } + await this.code.wait(100); await this.code.dispatchKeybinding(altKey ? 'Alt+Enter' : 'enter'); await this.quickinput.waitForQuickInputClosed(); if (commandId === TerminalCommandIdWithValue.NewWithProfile) { diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 3af2d83bccaef..2613f10da6285 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -105,6 +105,10 @@ async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWith console.error(`Error saving web client logs (${error})`); } + if (args.debug) { + return; + } + try { await browser.close(); } catch (error) { @@ -188,11 +192,11 @@ async function launchServer(browserType: BrowserType): Promise<{ endpoint: url.U serverArgs.push('--logsPath', serverLogsPath); const stdio: cp.StdioOptions = args.debug ? 'pipe' : ['ignore', 'pipe', 'ignore']; - + const shell: boolean = (process.platform === 'win32'); const serverProcess = cp.spawn( serverLocation, serverArgs, - { env, stdio } + { env, stdio, shell } ); if (args.debug) { diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000000000..a0df0c867783a --- /dev/null +++ b/test/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index 92c75f14404af..78f79b61838f8 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -19,6 +19,17 @@ export function setup(logger: Logger) { retry(async () => cp.execSync('git reset --hard HEAD --quiet', { cwd: app.workspacePathOrFolder }), 0, 5); }); + it('verifies the sidebar moves to the right', async function () { + const app = this.app as Application; + await app.workbench.search.openSearchViewlet(); + + await app.code.dispatchKeybinding('PageUp'); + await app.workbench.search.hasActivityBarMoved(); + + await app.code.dispatchKeybinding('PageUp'); + await app.workbench.search.hasActivityBarMoved(); + }); + it('searches for body & checks for correct result number', async function () { const app = this.app as Application; await app.workbench.search.openSearchViewlet(); diff --git a/test/smoke/src/areas/terminal/terminal-tabs.test.ts b/test/smoke/src/areas/terminal/terminal-tabs.test.ts index cbe672bbc1f2d..4ff156c6069d0 100644 --- a/test/smoke/src/areas/terminal/terminal-tabs.test.ts +++ b/test/smoke/src/areas/terminal/terminal-tabs.test.ts @@ -36,7 +36,8 @@ export function setup() { await terminal.assertSingleTab({ name }); }); - it('should reset the tab name to the default value when no name is provided', async () => { + // TODO: Flaky https://github.com/microsoft/vscode/issues/216564 + it.skip('should reset the tab name to the default value when no name is provided', async () => { await terminal.createTerminal(); const defaultName = await terminal.getSingleTabName(); const name = 'my terminal name'; diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index 322e5b7d510da..b0a0f4ddb23e0 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -246,6 +246,18 @@ async function runTestsInBrowser(testModules, browserType) { await page.goto(target.href); + if (args.build) { + const nlsMessages = await fs.promises.readFile(path.join(out, 'nls.messages.json'), 'utf8'); + await page.evaluate(value => { + // when running from `out-build`, ensure to load the default + // messages file, because all `nls.localize` calls have their + // english values removed and replaced by an index. + // VSCODE_GLOBALS: NLS + // @ts-ignore + globalThis._VSCODE_NLS_MESSAGES = JSON.parse(value); + }, nlsMessages); + } + page.on('console', async msg => { consoleLogFn(msg)(msg.text(), await Promise.all(msg.args().map(async arg => await arg.jsonValue()))); }); diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index c9f4dfa1e6a27..d4d85a185219b 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -97,6 +97,16 @@ const _tests_glob = '**/test/**/*.test.js'; let loader; let _out; +function initNls(opts) { + if (opts.build) { + // when running from `out-build`, ensure to load the default + // messages file, because all `nls.localize` calls have their + // english values removed and replaced by an index. + // VSCODE_GLOBALS: NLS + globalThis._VSCODE_NLS_MESSAGES = (require.__$__nodeRequire ?? require)(`../../../out-build/nls.messages.json`); + } +} + function initLoader(opts) { const outdir = opts.build ? 'out-build' : 'out'; _out = path.join(__dirname, `../../../${outdir}`); @@ -203,11 +213,17 @@ async function loadTests(opts) { 'throw ListenerLeakError' ]); + const _allowedSuitesWithOutput = new Set([ + 'InteractiveChatController' + ]); + let _testsWithUnexpectedOutput = false; for (const consoleFn of [console.log, console.error, console.info, console.warn, console.trace, console.debug]) { console[consoleFn.name] = function (msg) { - if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title)) { + if (!currentTest) { + consoleFn.apply(console, arguments); + } else if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title) && !_allowedSuitesWithOutput.has(currentTest.parent?.title)) { _testsWithUnexpectedOutput = true; consoleFn.apply(console, arguments); } @@ -432,6 +448,7 @@ function runTests(opts) { } ipcRenderer.on('run', (e, opts) => { + initNls(opts); initLoader(opts); runTests(opts).catch(err => { if (typeof err !== 'string') { diff --git a/test/unit/node/index.js b/test/unit/node/index.js index 14ceed7177cb8..9f0ec662a7345 100644 --- a/test/unit/node/index.js +++ b/test/unit/node/index.js @@ -84,6 +84,14 @@ function main() { globalThis._VSCODE_PRODUCT_JSON = require(`${REPO_ROOT}/product.json`); globalThis._VSCODE_PACKAGE_JSON = require(`${REPO_ROOT}/package.json`); + if (args.build) { + // when running from `out-build`, ensure to load the default + // messages file, because all `nls.localize` calls have their + // english values removed and replaced by an index. + // VSCODE_GLOBALS: NLS + globalThis._VSCODE_NLS_MESSAGES = require(`../../../${out}/nls.messages.json`); + } + // Test file operations that are common across platforms. Used for test infra, namely snapshot tests Object.assign(globalThis, { __analyzeSnapshotInTests: takeSnapshotAndCountClasses, @@ -228,7 +236,7 @@ function main() { // set up last test Mocha.suite('Loader', function () { test('should not explode while loading', function () { - assert.ok(!didErr, 'should not explode while loading'); + assert.ok(!didErr, `should not explode while loading: ${didErr}`); }); }); } diff --git a/yarn.lock b/yarn.lock index f2613ceef5146..f31bd180a3b9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -902,19 +902,19 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@playwright/browser-chromium@^1.40.1": - version "1.40.1" - resolved "https://registry.yarnpkg.com/@playwright/browser-chromium/-/browser-chromium-1.40.1.tgz#74f3a213f082798c838f2ef1405c5e9cfeeebc99" - integrity sha512-uNmjHYXBTYTfJkf89D6zVUcesCFzZ/yjkPj8FvBJQ6yf3na/j1rcjVNzx0PzOAGcWKioB/rnWRBi7b5ojOdCHA== +"@playwright/browser-chromium@^1.45.0": + version "1.45.1" + resolved "https://registry.yarnpkg.com/@playwright/browser-chromium/-/browser-chromium-1.45.1.tgz#7c1a69dfe9969df6271e8174f9c5dd43dee9a2a3" + integrity sha512-Jcix36xOagtda46bfn0CcdnygrcFT7FfwLhF1Y8JkHZTKPgfiku695+F0+CWabL4sU9nBG8sT5l7dmgkMgAJgw== dependencies: - playwright-core "1.40.1" + playwright-core "1.45.1" -"@playwright/test@^1.40.1": - version "1.40.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.40.1.tgz#9e66322d97b1d74b9f8718bacab15080f24cde65" - integrity sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw== +"@playwright/test@^1.45.0": + version "1.45.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.45.0.tgz#790a66165a46466c0d7099dd260881802f5aba7e" + integrity sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw== dependencies: - playwright "1.40.1" + playwright "1.45.0" "@sindresorhus/is@^4.0.0": version "4.6.0" @@ -1029,11 +1029,6 @@ dependencies: defer-to-connect "^2.0.0" -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - "@tootallnate/once@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-3.0.0.tgz#d52238c9052d746c9689523e650160e70786bc9a" @@ -1145,13 +1140,6 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/graceful-fs@4.1.2": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.2.tgz#fbc9575dbcc6d1d91dd768d30c5fc0c19f6c50bd" - integrity sha512-epDhsJAVxJsWfeqpzEDFhLnhHMbHie/VMFY+2Hvt5p7FemeW5ELM+6gcVYL/ZsUwdu3zrWpDE3VUTddXW+EMYg== - dependencies: - "@types/node" "*" - "@types/gulp-svgmin@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/gulp-svgmin/-/gulp-svgmin-1.2.1.tgz#e18f344ea09560554652406b37e1dc3253a6bda2" @@ -1577,10 +1565,10 @@ bindings "^1.5.0" node-addon-api "^6.0.0" -"@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" @@ -1646,34 +1634,36 @@ supports-color "^9.4.0" yargs "^17.7.2" -"@vscode/test-electron@^2.3.8": - version "2.3.8" - resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.3.8.tgz#06a7c50b38cfac0ede833905e088d55c61cd12d3" - integrity sha512-b4aZZsBKtMGdDljAsOPObnAi7+VWIaYl3ylCz1jTs+oV6BZ4TNHcVNC3xUn0azPeszBmwSBDQYfFESIaUQnrOg== +"@vscode/test-electron@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@vscode/test-electron/-/test-electron-2.4.0.tgz#6fcdbac10948960c15f8970cf5d5e624dd51a524" + integrity sha512-yojuDFEjohx6Jb+x949JRNtSn6Wk2FAh4MldLE3ck9cfvCqzwxF32QsNy1T9Oe4oT+ZfFcg0uPUCajJzOmPlTA== dependencies: - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.4" jszip "^3.10.1" - semver "^7.5.2" + ora "^7.0.1" + semver "^7.6.2" -"@vscode/test-web@^0.0.50": - version "0.0.50" - resolved "https://registry.yarnpkg.com/@vscode/test-web/-/test-web-0.0.50.tgz#fe6a9f5a50f3d8fd9379593a44173696ea75c20c" - integrity sha512-aFcwTyA3qirjfr6f5oKEV/07MbsbNeJEqElIh/ceNPEnJIyKYQeffbU7Cia9XtTKk1Ue1qMqwhdIO8OnUa9QBg== +"@vscode/test-web@^0.0.56": + version "0.0.56" + resolved "https://registry.yarnpkg.com/@vscode/test-web/-/test-web-0.0.56.tgz#963b6e1bfa6d5b4b600083e328ee8d28890108eb" + integrity sha512-lR688n+D6A9odw+IZ5cU8CYr2YXLB61bGgyZpPVJe/sJy4/DYX5CAxPb7Wj9ZMYL41CTvWq5DeXtfCjlabPcYA== dependencies: "@koa/cors" "^5.0.0" "@koa/router" "^12.0.1" - "@playwright/browser-chromium" "^1.40.1" + "@playwright/browser-chromium" "^1.45.0" + glob "^10.4.2" gunzip-maybe "^1.4.2" - http-proxy-agent "^7.0.0" - https-proxy-agent "^7.0.2" - koa "^2.14.2" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.4" + koa "^2.15.3" koa-morgan "^1.0.1" koa-mount "^4.0.0" koa-static "^5.0.0" minimist "^1.2.8" - playwright "^1.40.1" - tar-fs "^3.0.4" + playwright "^1.45.0" + tar-fs "^3.0.6" vscode-uri "^3.0.8" "@vscode/v8-heap-parser@^0.1.0": @@ -1982,40 +1972,47 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== -"@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-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-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-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-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/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/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-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-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-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-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-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/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/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== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -2085,13 +2082,6 @@ acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== -agent-base@6: - version "6.0.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" - integrity sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg== - dependencies: - debug "4" - agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" @@ -2099,6 +2089,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" + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2201,11 +2198,6 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -2503,6 +2495,39 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bare-events@^2.0.0, bare-events@^2.2.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.4.2.tgz#3140cca7a0e11d49b3edc5041ab560659fd8e1f8" + integrity sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q== + +bare-fs@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-2.3.1.tgz#cdbd63dac7a552dfb2b87d18c822298d1efd213d" + integrity sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA== + dependencies: + bare-events "^2.0.0" + bare-path "^2.0.0" + bare-stream "^2.0.0" + +bare-os@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-2.4.0.tgz#5de5e3ba7704f459c9656629edca7cc736e06608" + integrity sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg== + +bare-path@^2.0.0, bare-path@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-2.1.3.tgz#594104c829ef660e43b5589ec8daef7df6cedb3e" + integrity sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA== + dependencies: + bare-os "^2.1.0" + +bare-stream@^2.0.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.1.3.tgz#070b69919963a437cc9e20554ede079ce0a129b2" + integrity sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ== + dependencies: + streamx "^2.18.0" + base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -2569,6 +2594,15 @@ bl@^4.0.2, bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +bl@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-5.1.0.tgz#183715f678c7188ecef9fe475d90209400624273" + integrity sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ== + dependencies: + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^3.4.0" + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -2618,11 +2652,11 @@ braces@^2.3.1, braces@^2.3.2: to-regex "^3.0.1" braces@^3.0.1, braces@^3.0.2, 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" browser-stdout@1.3.1: version "1.3.1" @@ -2700,6 +2734,14 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtin-modules@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" @@ -2870,6 +2912,11 @@ chalk@^4.x: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.0.0, chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -2966,6 +3013,18 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + +cli-spinners@^2.9.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + cli-width@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" @@ -3263,10 +3322,10 @@ cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookies@~0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" - integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== +cookies@~0.9.0: + version "0.9.1" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.9.1.tgz#3ffed6f60bb4fb5f146feeedba50acc418af67e3" + integrity sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw== dependencies: depd "~2.0.0" keygrip "~1.1.0" @@ -3610,6 +3669,11 @@ defer-to-connect@^2.0.0: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -3853,15 +3917,20 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.717.tgz#99db370cae8cd090d5b01f8748e9ad369924d0f8" integrity sha512-6Fmg8QkkumNOwuZ/5mIbMU9WI3H2fmn5ajcVya64I5Yr5CcNmO7vcLt0Y7c96DCiMO5/9G+4sI2r6eEvdg1F7A== -electron@29.4.0: - version "29.4.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-29.4.0.tgz#5dcd5a977414337a2518619e9166c0e86a5a3bae" - integrity sha512-4DTO8U66oiI8rShrDSu2zDPW6GWRiCebyb1MHSfQkLWCNI/PnLyGKeqYPUoVgc0FWaNN2sCBn8NKJHb++hE2LQ== +electron@30.1.2: + version "30.1.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-30.1.2.tgz#9c8b9b0d0e3f07783d8c5dbd9519b3ffd11f1551" + integrity sha512-A5CFGwbA+HSXnzwjc8fP2GIezBcAb0uN/VbNGLOW8DHOYn07rvJ/1bAJECHUUzt5zbfohveG3hpMQiYpbktuDw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^20.9.0" extract-zip "^2.0.1" +emoji-regex@^10.2.1: + version "10.3.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.3.0.tgz#76998b9268409eb3dae3de989254d456e70cfe23" + integrity sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -4457,7 +4526,7 @@ fast-fifo@^1.1.0: resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.2.0.tgz#2ee038da2468e8623066dee96958b0c1763aa55a" integrity sha512-NcvQXt7Cky1cNau15FWy64IjuO8X0JijhTBBrJj1YlxlDfRkJXNaK9RFUjwpfDPzMdv7wB38jr53l9tkNLxnWg== -fast-fifo@^1.2.0: +fast-fifo@^1.2.0, fast-fifo@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== @@ -4569,10 +4638,10 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.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" @@ -4975,6 +5044,18 @@ glob@^10.0.0, glob@^10.3.10: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" +glob@^10.4.2: + 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 "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^5.0.13: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -5127,11 +5208,6 @@ got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -graceful-fs@4.2.11, graceful-fs@^4.2.11: - 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== - graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" @@ -5142,6 +5218,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@^4.2.11: + 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== + graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -5535,15 +5616,6 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - http-proxy-agent@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz#e9096c5afd071a3fce56e6252bb321583c124673" @@ -5552,6 +5624,14 @@ http-proxy-agent@^7.0.0: agent-base "^7.1.0" debug "^4.3.4" +http-proxy-agent@^7.0.2: + 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: + agent-base "^7.1.0" + debug "^4.3.4" + http2-wrapper@^1.0.0-beta.5.2: version "1.0.3" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" @@ -5560,14 +5640,6 @@ http2-wrapper@^1.0.0-beta.5.2: quick-lru "^5.1.1" resolve-alpn "^1.0.0" -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== - dependencies: - agent-base "6" - debug "4" - https-proxy-agent@^7.0.0: version "7.0.4" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz#8e97b841a029ad8ddc8731f26595bad868cb4168" @@ -5584,6 +5656,14 @@ https-proxy-agent@^7.0.2: agent-base "^7.0.2" debug "4" +https-proxy-agent@^7.0.4: + 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 "^7.0.2" + debug "4" + husky@^0.13.1: version "0.13.4" resolved "https://registry.yarnpkg.com/husky/-/husky-0.13.4.tgz#48785c5028de3452a51c48c12c4f94b2124a1407" @@ -5611,7 +5691,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -5729,10 +5809,13 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== -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-absolute@^1.0.0: version "1.0.0" @@ -5868,6 +5951,11 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -5933,6 +6021,11 @@ is-gzip@^1.0.0: resolved "https://registry.yarnpkg.com/is-gzip/-/is-gzip-1.0.0.tgz#6ca8b07b99c77998025900e555ced8ed80879a83" integrity sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ== +is-interactive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90" + integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== + is-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" @@ -6037,6 +6130,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-unicode-supported@^1.1.0, is-unicode-supported@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + is-utf8@^0.2.0, is-utf8@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -6057,6 +6155,13 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + is@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" @@ -6170,6 +6275,15 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +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" + jest-worker@^27.0.2: version "27.0.6" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed" @@ -6193,6 +6307,11 @@ js-base64@^3.7.4: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.4.tgz#af95b20f23efc8034afd2d1cc5b9d0adf7419037" integrity sha512-wpM/wi20Tl+3ifTyi0RdDckS4YTD4Lf953mBRrpG8547T7hInHNPEj8+ck4gB8VDcGyeAWFK++Wb/fU1BeavKQ== +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== + js-beautify@^1.8.9: version "1.8.9" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.8.9.tgz#08e3c05ead3ecfbd4f512c3895b1cda76c87d523" @@ -6229,10 +6348,15 @@ js-yaml@^3.13.0: argparse "^1.0.7" esprima "^4.0.0" -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== +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== jsdoc-type-pratt-parser@~4.0.0: version "4.0.0" @@ -6320,14 +6444,14 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== -kerberos@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.0.1.tgz#663b0b46883b4da84495f60f2e9e399a43a33ef5" - integrity sha512-O/jIgbdGK566eUhFwIcgalbqirYU/r76MW7/UFw06Fd9x5bSwgyZWL/Vm26aAmezQww/G9KYkmmJBkEkPk5HLw== +kerberos@2.1.1-alpha.0: + version "2.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/kerberos/-/kerberos-2.1.1-alpha.0.tgz#c6d377b43c8206340fd184167754f2c81dad5ab1" + integrity sha512-II8N/ky/Vpd8y7LTxwCuAYoQ8XeV3HYxuK7IDmyoFacIhDljx4sdt/+sOwqgXEweQyCHlbZSKSaK82upqNM4Hw== dependencies: bindings "^1.5.0" - node-addon-api "^4.3.0" - prebuild-install "7.1.1" + node-addon-api "^6.1.0" + prebuild-install "^7.1.2" keygrip@~1.1.0: version "1.1.0" @@ -6417,16 +6541,16 @@ koa-static@^5.0.0: debug "^3.1.0" koa-send "^5.0.0" -koa@^2.14.2: - version "2.14.2" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.14.2.tgz#a57f925c03931c2b4d94b19d2ebf76d3244863fc" - integrity sha512-VFI2bpJaodz6P7x2uyLiX6RLYpZmOJqNmoCst/Yyd7hQlszyPwG/I9CQJ63nOtKSxpt5M7NH67V6nJL2BwCl7g== +koa@^2.15.3: + version "2.15.3" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.15.3.tgz#062809266ee75ce0c75f6510a005b0e38f8c519a" + integrity sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg== dependencies: accepts "^1.3.5" cache-content-type "^1.0.0" content-disposition "~0.5.2" content-type "^1.0.4" - cookies "~0.8.0" + cookies "~0.9.0" debug "^4.3.2" delegates "^1.0.0" depd "^2.0.0" @@ -6646,11 +6770,24 @@ log-symbols@4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +log-symbols@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-5.1.0.tgz#a20e3b9a5f53fac6aeb8e2bb22c07cf2c8f16d93" + integrity sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA== + dependencies: + chalk "^5.0.0" + is-unicode-supported "^1.1.0" + lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +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@^4.1.3: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -6948,7 +7085,7 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-fn@^2.0.0: +mimic-fn@^2.0.0, mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -6991,6 +7128,13 @@ minimatch@^7.4.3: dependencies: brace-expansion "^2.0.1" +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" + minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" @@ -7018,6 +7162,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +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: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -7268,16 +7417,16 @@ node-addon-api@^4.2.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87" integrity sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q== -node-addon-api@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" - integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== - node-addon-api@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.0.0.tgz#cfb3574e6df708ff71a30db6c4762d9e06e11c27" integrity sha512-GyHvgPvUXBvAkXa0YvYnhilSB1A+FRYMpIVggKzPZqdaZfevZOuzfWzyvgzOwRLHBeo/MMswmJFsrNF4Nw1pmA== +node-addon-api@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== + node-fetch@2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.8.tgz#a68d30b162bc1d8fd71a367e81b997e1f4d4937e" @@ -7520,11 +7669,27 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + only@~0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= +open@^8.4.2: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + opn@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/opn/-/opn-6.0.0.tgz#3c5b0db676d5f97da1233d1ed42d182bc5a27d2d" @@ -7556,6 +7721,21 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +ora@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-7.0.1.tgz#cdd530ecd865fe39e451a0e7697865669cb11930" + integrity sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw== + dependencies: + chalk "^5.3.0" + cli-cursor "^4.0.0" + cli-spinners "^2.9.0" + is-interactive "^2.0.0" + is-unicode-supported "^1.3.0" + log-symbols "^5.1.0" + stdin-discarder "^0.1.0" + string-width "^6.1.0" + strip-ansi "^7.1.0" + ordered-read-streams@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" @@ -7669,6 +7849,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +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== + pako@~0.2.0: version "0.2.9" resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" @@ -7802,6 +7987,14 @@ path-scurry@^1.10.1: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +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 "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" @@ -7920,17 +8113,22 @@ playwright-core@1.30.0: resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.30.0.tgz#de987cea2e86669e3b85732d230c277771873285" integrity sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g== -playwright-core@1.40.1: - version "1.40.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.40.1.tgz#442d15e86866a87d90d07af528e0afabe4c75c05" - integrity sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ== +playwright-core@1.45.0: + version "1.45.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.0.tgz#5741a670b7c9060ce06852c0051d84736fb94edc" + integrity sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ== + +playwright-core@1.45.1: + version "1.45.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.1.tgz#549a2701556b58245cc75263f9fc2795c1158dc1" + integrity sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg== -playwright@1.40.1, playwright@^1.40.1: - version "1.40.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.40.1.tgz#a11bf8dca15be5a194851dbbf3df235b9f53d7ae" - integrity sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw== +playwright@1.45.0: + version "1.45.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.0.tgz#400c709c64438690f13705cb9c88ef93089c5c27" + integrity sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA== dependencies: - playwright-core "1.40.1" + playwright-core "1.45.0" optionalDependencies: fsevents "2.3.2" @@ -7941,6 +8139,15 @@ playwright@^1.29.2: dependencies: playwright-core "1.30.0" +playwright@^1.45.0: + version "1.45.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.1.tgz#aaa6b0d6db14796b599d80c6679e63444e942534" + integrity sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg== + dependencies: + playwright-core "1.45.1" + optionalDependencies: + fsevents "2.3.2" + plist@^3.0.1: version "3.0.5" resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.5.tgz#2cbeb52d10e3cdccccf0c11a63a85d830970a987" @@ -8254,10 +8461,10 @@ postcss@^8.4.33: picocolors "^1.0.0" source-map-js "^1.2.0" -prebuild-install@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== +prebuild-install@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" + integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== dependencies: detect-libc "^2.0.0" expand-template "^2.0.3" @@ -8723,6 +8930,14 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -8874,7 +9089,7 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.4: +semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -8888,6 +9103,11 @@ semver@^7.5.3: dependencies: lru-cache "^6.0.0" +semver@^7.6.2: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" @@ -9076,20 +9296,20 @@ snapdragon@^0.8.1: use "^3.1.0" 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" source-map-js@^1.0.1: @@ -9228,6 +9448,11 @@ sprintf-js@^1.1.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== +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== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -9261,6 +9486,13 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stdin-discarder@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz#22b3e400393a8e28ebf53f9958f3880622efde21" + integrity sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ== + dependencies: + bl "^5.0.0" + stream-combiner@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" @@ -9321,7 +9553,18 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: +streamx@^2.18.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.18.0.tgz#5bc1a51eb412a667ebfdcd4e6cf6a6fc65721ac7" + integrity sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ== + dependencies: + fast-fifo "^1.3.2" + queue-tick "^1.0.1" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + +"string-width-cjs@npm:string-width@^4.2.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== @@ -9356,14 +9599,14 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== +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.0" + strip-ansi "^6.0.1" string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" @@ -9374,6 +9617,15 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-6.1.0.tgz#96488d6ed23f9ad5d82d13522af9e4c4c3fd7518" + integrity sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^10.2.1" + strip-ansi "^7.0.1" + string.prototype.padend@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.1.tgz#824c84265dbac46cade2b957b38b6a5d8d1683c5" @@ -9418,7 +9670,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: +"strip-ansi-cjs@npm: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== @@ -9446,14 +9698,14 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +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.0" + ansi-regex "^5.0.1" -strip-ansi@^7.0.1: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -9614,14 +9866,16 @@ tar-fs@^2.0.0: pump "^3.0.0" tar-stream "^2.1.4" -tar-fs@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.4.tgz#a21dc60a2d5d9f55e0089ccd78124f1d3771dbbf" - integrity sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w== +tar-fs@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.6.tgz#eaccd3a67d5672f09ca8e8f9c3d2b89fa173f217" + integrity sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w== dependencies: - mkdirp-classic "^0.5.2" pump "^3.0.0" tar-stream "^3.1.5" + optionalDependencies: + bare-fs "^2.1.1" + bare-path "^2.1.0" tar-stream@^2.1.4: version "2.2.0" @@ -9734,6 +9988,13 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-decoder@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.1.0.tgz#3379e728fcf4d3893ec1aea35e8c2cac215ef190" + integrity sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw== + dependencies: + b4a "^1.6.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -10055,10 +10316,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.5.0-dev.20240521: - version "5.5.0-dev.20240521" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240521.tgz#a53f71ad2f5e4c4401a56c35993474b77813364c" - integrity sha512-52WLKX9mbRmStK1lb30KM78dSo5ssgQT8WQERYiv8JihXir4HUgwlgTz4crExojzpsGjFGFJROL/bZrhXUiOEQ== +typescript@^5.6.0-dev.20240703: + version "5.6.0-dev.20240703" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.0-dev.20240703.tgz#4b1fad9790ff4827a9210c6943b3039b53a739da" + integrity sha512-AAOGWtykhMpxB4l+5CwojT2aBVAszalz9guzYaZMavmKHWxm3HciR+cIcKqDgR22hR7fPBJHtOti7uFCo4mt4A== typical@^4.0.0: version "4.0.0" @@ -10608,7 +10869,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm: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== @@ -10643,6 +10904,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +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" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -10665,9 +10935,9 @@ write@1.0.3: mkdirp "^0.5.1" ws@^7.2.0: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== xml2js@^0.4.19: version "0.4.23"