diff --git a/.github/commands.json b/.github/commands.json index 7b04c7475d7..df9fc791fd5 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -156,37 +156,6 @@ "addLabel": "confirmation-pending", "removeLabel": "confirmed" }, - { - "type": "comment", - "name": "needsMoreInfo", - "allowUsers": [ - "cleidigh", - "usernamehw", - "gjsjohnmurray", - "IllusionMH" - ], - "action": "updateLabels", - "addLabel": "~info-needed" - }, - { - "type": "comment", - "name": "needsPerfInfo", - "allowUsers": [ - "cleidigh", - "usernamehw", - "gjsjohnmurray", - "IllusionMH" - ], - "addLabel": "info-needed", - "comment": "Thanks for creating this issue regarding performance! Please follow this guide to help us diagnose performance issues: https://github.com/microsoft/vscode/wiki/Performance-Issues \n\nHappy Coding!" - }, - { - "type": "comment", - "name": "jsDebugLogs", - "action": "updateLabels", - "addLabel": "info-needed", - "comment": "Please collect trace logs using the following instructions:\n\n> If you're able to, add `\"trace\": true` to your `launch.json` and reproduce the issue. The location of the log file on your disk will be written to the Debug Console. Share that with us.\n>\n> ⚠️ This log file will not contain source code, but will contain file paths. You can drop it into https://microsoft.github.io/vscode-pwa-analyzer/index.html to see what it contains. If you'd rather not share the log publicly, you can email it to connor@xbox.com" - }, { "type": "comment", "name": "closedWith", @@ -200,30 +169,6 @@ "reason": "completed", "addLabel": "unreleased" }, - { - "type": "label", - "name": "~info-needed", - "action": "updateLabels", - "addLabel": "info-needed", - "removeLabel": "~info-needed", - "comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" - }, - { - "type": "label", - "name": "~version-info-needed", - "action": "updateLabels", - "addLabel": "info-needed", - "removeLabel": "~version-info-needed", - "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!" - }, - { - "type": "label", - "name": "~confirmation-needed", - "action": "updateLabels", - "addLabel": "info-needed", - "removeLabel": "~confirmation-needed", - "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" - }, { "type": "comment", "name": "a11ymas", @@ -469,32 +414,6 @@ "addLabel": "*caused-by-extension", "comment": "It looks like this is caused by the Copilot extension. Please file the issue in the [Copilot Discussion Forum](https://github.com/community/community/discussions/categories/copilot). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting) for more information.\n\nHappy Coding!" }, - { - "type": "comment", - "name": "gifPlease", - "allowUsers": [ - "cleidigh", - "usernamehw", - "gjsjohnmurray", - "IllusionMH" - ], - "action": "comment", - "addLabel": "info-needed", - "comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette). Lastly, please attach this file via the GitHub web interface as emailed responses will strip files out from the issue.\n\nHappy coding!" - }, - { - "type": "comment", - "name": "confirmPlease", - "allowUsers": [ - "cleidigh", - "usernamehw", - "gjsjohnmurray", - "IllusionMH" - ], - "action": "comment", - "addLabel": "info-needed", - "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" - }, { "__comment__": "Allows folks on the team to label issues by commenting: `\\label My-Label` ", "type": "comment", diff --git a/.nvmrc b/.nvmrc index a9d087399d7..bc78e9f2695 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.19.0 +20.12.1 diff --git a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json new file mode 100644 index 00000000000..deb2c584c47 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "configurations": [ + { + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--enable-proposed-api=ms-vscode.vscode-selfhost-test-provider"], + "name": "Launch Extension", + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "request": "launch", + "type": "extensionHost" + } + ] +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json new file mode 100644 index 00000000000..e4429caeee4 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index f27953f3cdb..f472098cd14 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -3,7 +3,8 @@ "displayName": "VS Code Selfhost Test Provider", "description": "Test provider for the VS Code project", "enabledApiProposals": [ - "testObserver" + "testObserver", + "attributableCoverage" ], "engines": { "vscode": "^1.88.0" @@ -13,6 +14,13 @@ { "command": "selfhost-test-provider.updateSnapshot", "title": "Update Snapshot", + "category": "Testing", + "icon": "$(merge)" + }, + { + "command": "selfhost-test-provider.openFailureLog", + "title": "Open Selfhost Failure Logs", + "category": "Testing", "icon": "$(merge)" } ], @@ -65,10 +73,12 @@ "license": "MIT", "scripts": { "compile": "gulp compile-extension:vscode-selfhost-test-provider", - "watch": "gulp watch-extension:vscode-selfhost-test-provider" + "watch": "gulp watch-extension:vscode-selfhost-test-provider", + "test": "npx mocha --ui tdd 'out/*.test.js'" }, "devDependencies": { - "@types/node": "18.x" + "@types/mocha": "^10.0.6", + "@types/node": "20.x" }, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts index 16fa7843336..7280782c10a 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/coverageProvider.ts @@ -4,5 +4,154 @@ *--------------------------------------------------------------------------------------------*/ import { IstanbulCoverageContext } from 'istanbul-to-vscode'; +import * as vscode from 'vscode'; +import { SourceLocationMapper, SourceMapStore } from './testOutputScanner'; +import { IScriptCoverage, OffsetToPosition, RangeCoverageTracker } from './v8CoverageWrangling'; -export const coverageContext = new IstanbulCoverageContext(); +export const istanbulCoverageContext = new IstanbulCoverageContext(); + +/** + * Tracks coverage in per-script coverage mode. There are two modes of coverage + * in this extension: generic istanbul reports, and reports from the runtime + * sent before and after each test case executes. This handles the latter. + */ +export class PerTestCoverageTracker { + private readonly scripts = new Map(); + + constructor(private readonly maps: SourceMapStore) {} + + public add(coverage: IScriptCoverage, test?: vscode.TestItem) { + const script = this.scripts.get(coverage.scriptId); + if (script) { + return script.add(coverage, test); + } + // ignore internals and node_modules + if (!coverage.url.startsWith('file://') || coverage.url.includes('node_modules')) { + return; + } + if (!coverage.source) { + throw new Error('expected to have source the first time a script is seen'); + } + + const src = new Script(vscode.Uri.parse(coverage.url), coverage.source, this.maps); + this.scripts.set(coverage.scriptId, src); + src.add(coverage, test); + } + + public async report(run: vscode.TestRun) { + await Promise.all(Array.from(this.scripts.values()).map(s => s.report(run))); + } +} + +class Script { + private converter: OffsetToPosition; + + /** Tracking the overall coverage for the file */ + private overall = new ScriptCoverageTracker(); + /** Range tracking per-test item */ + private readonly perItem = new Map(); + + constructor( + public readonly uri: vscode.Uri, + source: string, + private readonly maps: SourceMapStore + ) { + this.converter = new OffsetToPosition(source); + } + + public add(coverage: IScriptCoverage, test?: vscode.TestItem) { + this.overall.add(coverage); + if (test) { + const p = new ScriptCoverageTracker(); + p.add(coverage); + this.perItem.set(test, p); + } + } + + 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)); + } + } +} + +class ScriptCoverageTracker { + private coverage = new RangeCoverageTracker(); + + public add(coverage: IScriptCoverage) { + for (const range of RangeCoverageTracker.initializeBlocks(coverage.functions)) { + this.coverage.setCovered(range.start, range.end, range.covered); + } + } + + /** + * 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, + item?: vscode.TestItem + ): V8CoverageFile { + const file = new V8CoverageFile(uri, item); + + for (const range of this.coverage) { + if (range.start === range.end) { + continue; + } + + const startCov = convert.toLineColumn(range.start); + let start = new vscode.Position(startCov.line, startCov.column); + + const endCov = convert.toLineColumn(range.end); + let end = new vscode.Position(endCov.line, endCov.column); + if (mapper) { + const startMap = mapper(start.line, start.character); + const endMap = startMap && mapper(end.line, end.character); + if (!endMap || uri.toString().toLowerCase() !== endMap.uri.toString().toLowerCase()) { + continue; + } + start = startMap.range.start; + end = endMap.range.end; + } + + for (let i = start.line; i <= end.line; i++) { + file.add( + new vscode.StatementCoverage( + range.covered, + new vscode.Range( + new vscode.Position(i, i === start.line ? start.character : 0), + new vscode.Position(i, i === end.line ? end.character : Number.MAX_SAFE_INTEGER) + ) + ) + ); + } + } + + return file; + } +} + +export class V8CoverageFile extends vscode.FileCoverage { + public details: vscode.StatementCoverage[] = []; + + constructor(uri: vscode.Uri, item?: vscode.TestItem) { + super(uri, { covered: 0, total: 0 }); + (this as vscode.FileCoverage2).testItem = item; + } + + public add(detail: vscode.StatementCoverage) { + this.details.push(detail); + this.statementCoverage.total++; + if (detail.executed) { + this.statementCoverage.covered++; + } + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts index b4fa3310545..960dbcf634e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/extension.ts @@ -7,8 +7,9 @@ import { randomBytes } from 'crypto'; import { tmpdir } from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; -import { coverageContext } from './coverageProvider'; +import { V8CoverageFile } from './coverageProvider'; import { FailingDeepStrictEqualAssertFixer } from './failingDeepStrictEqualAssertFixer'; +import { FailureTracker } from './failureTracker'; import { registerSnapshotUpdate } from './snapshot'; import { scanTestOutput } from './testOutputScanner'; import { @@ -40,6 +41,17 @@ export async function activate(context: vscode.ExtensionContext) { const ctrl = vscode.tests.createTestController('selfhost-test-controller', 'VS Code Tests'); const fileChangedEmitter = new vscode.EventEmitter(); + context.subscriptions.push(vscode.tests.registerTestFollowupProvider({ + async provideFollowup(_result, test, taskIndex, messageIndex, _token) { + return [{ + title: '$(sparkle) Ask copilot for help', + command: 'github.copilot.tests.fixTestFailure', + arguments: [{ source: 'peekFollowup', test, message: test.taskStates[taskIndex].messages[messageIndex] }] + }]; + }, + })); + + ctrl.resolveHandler = async test => { if (!test) { context.subscriptions.push(await startWatchingWorkspace(ctrl, fileChangedEmitter)); @@ -54,6 +66,12 @@ export async function activate(context: vscode.ExtensionContext) { } }; + guessWorkspaceFolder().then(folder => { + if (folder) { + context.subscriptions.push(new FailureTracker(context, folder.uri.fsPath)); + } + }); + const createRunHandler = ( runnerCtor: { new(folder: vscode.WorkspaceFolder): VSCodeTestRunner }, kind: vscode.TestRunProfileKind, @@ -78,17 +96,23 @@ export async function activate(context: vscode.ExtensionContext) { let coverageDir: string | undefined; let currentArgs = args; if (kind === vscode.TestRunProfileKind.Coverage) { - coverageDir = path.join(tmpdir(), `vscode-test-coverage-${randomBytes(8).toString('hex')}`); - currentArgs = [ - ...currentArgs, - '--coverage', - '--coveragePath', - coverageDir, - '--coverageFormats', - 'json', - '--coverageFormats', - 'html', - ]; + // todo: browser runs currently don't support per-test coverage + if (args.includes('--browser')) { + coverageDir = path.join( + tmpdir(), + `vscode-test-coverage-${randomBytes(8).toString('hex')}` + ); + currentArgs = [ + ...currentArgs, + '--coverage', + '--coveragePath', + coverageDir, + '--coverageFormats', + 'json', + ]; + } else { + currentArgs = [...currentArgs, '--per-test-coverage']; + } } return await scanTestOutput( @@ -172,7 +196,13 @@ export async function activate(context: vscode.ExtensionContext) { true ); - coverage.loadDetailedCoverage = coverageContext.loadDetailedCoverage; + coverage.loadDetailedCoverage = async (_run, coverage) => { + if (coverage instanceof V8CoverageFile) { + return coverage.details; + } + + return []; + }; for (const [name, arg] of browserArgs) { const cfg = ctrl.createRunProfile( diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts index b0494471f29..17e65cbce50 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failingDeepStrictEqualAssertFixer.ts @@ -5,81 +5,79 @@ import * as ts from 'typescript'; import { - commands, - Disposable, - languages, - Position, - Range, - TestMessage, - TestResultSnapshot, - TestRunResult, - tests, - TextDocument, - Uri, - workspace, - WorkspaceEdit, + commands, + Disposable, + languages, + Position, + Range, + TestMessage, + TestResultSnapshot, + TestRunResult, + tests, + TextDocument, + Uri, + workspace, + WorkspaceEdit, } from 'vscode'; import { memoizeLast } from './memoize'; import { getTestMessageMetadata } from './metadata'; const enum Constants { - FixCommandId = 'selfhost-test.fix-test', + FixCommandId = 'selfhost-test.fix-test', } export class FailingDeepStrictEqualAssertFixer { - private disposables: Disposable[] = []; + private disposables: Disposable[] = []; - constructor() { - this.disposables.push( - commands.registerCommand(Constants.FixCommandId, async (uri: Uri, position: Position) => { - const document = await workspace.openTextDocument(uri); + constructor() { + this.disposables.push( + commands.registerCommand(Constants.FixCommandId, async (uri: Uri, position: Position) => { + const document = await workspace.openTextDocument(uri); - const failingAssertion = detectFailingDeepStrictEqualAssertion(document, position); - if (!failingAssertion) { - return; - } + const failingAssertion = detectFailingDeepStrictEqualAssertion(document, position); + if (!failingAssertion) { + return; + } - const expectedValueNode = failingAssertion.assertion.expectedValue; - if (!expectedValueNode) { - return; - } + const expectedValueNode = failingAssertion.assertion.expectedValue; + if (!expectedValueNode) { + return; + } - const start = document.positionAt(expectedValueNode.getStart()); - const end = document.positionAt(expectedValueNode.getEnd()); + const start = document.positionAt(expectedValueNode.getStart()); + const end = document.positionAt(expectedValueNode.getEnd()); - const edit = new WorkspaceEdit(); - edit.replace(uri, new Range(start, end), formatJsonValue(failingAssertion.actualJSONValue)); - await workspace.applyEdit(edit); - }) - ); + const edit = new WorkspaceEdit(); + edit.replace(uri, new Range(start, end), formatJsonValue(failingAssertion.actualJSONValue)); + await workspace.applyEdit(edit); + }) + ); - this.disposables.push( - languages.registerCodeActionsProvider('typescript', { - provideCodeActions: (document, range) => { - const failingAssertion = detectFailingDeepStrictEqualAssertion(document, range.start); - if (!failingAssertion) { - return undefined; - } + this.disposables.push( + languages.registerCodeActionsProvider('typescript', { + provideCodeActions: (document, range) => { + const failingAssertion = detectFailingDeepStrictEqualAssertion(document, range.start); + if (!failingAssertion) { + return undefined; + } - return [ - { - title: 'Fix Expected Value', - command: Constants.FixCommandId, - arguments: [document.uri, range.start], - }, - ]; - }, - }) - ); + return [ + { + title: 'Fix Expected Value', + command: Constants.FixCommandId, + arguments: [document.uri, range.start], + }, + ]; + }, + }) + ); + } - tests.testResults; - } - - dispose() { - for (const d of this.disposables) { - d.dispose(); - } - } + dispose() { + for (const d of this.disposables) { + d.dispose(); + } + } } const identifierLikeRe = /^[$a-z_][a-z0-9_$]*$/i; @@ -87,170 +85,170 @@ const identifierLikeRe = /^[$a-z_][a-z0-9_$]*$/i; const tsPrinter = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const formatJsonValue = (value: unknown) => { - if (typeof value !== 'object') { - return JSON.stringify(value); - } + if (typeof value !== 'object') { + return JSON.stringify(value); + } - const src = ts.createSourceFile('', `(${JSON.stringify(value)})`, ts.ScriptTarget.ES5, true); - const outerExpression = src.statements[0] as ts.ExpressionStatement; - const parenExpression = outerExpression.expression as ts.ParenthesizedExpression; + const src = ts.createSourceFile('', `(${JSON.stringify(value)})`, ts.ScriptTarget.ES5, true); + const outerExpression = src.statements[0] as ts.ExpressionStatement; + const parenExpression = outerExpression.expression as ts.ParenthesizedExpression; - const unquoted = ts.transform(parenExpression, [ - context => (node: ts.Node) => { - const visitor = (node: ts.Node): ts.Node => - ts.isPropertyAssignment(node) && - ts.isStringLiteralLike(node.name) && - identifierLikeRe.test(node.name.text) - ? ts.factory.createPropertyAssignment( - ts.factory.createIdentifier(node.name.text), - ts.visitNode(node.initializer, visitor) as ts.Expression - ) - : ts.isStringLiteralLike(node) && node.text === '[undefined]' - ? ts.factory.createIdentifier('undefined') - : ts.visitEachChild(node, visitor, context); + const unquoted = ts.transform(parenExpression, [ + context => (node: ts.Node) => { + const visitor = (node: ts.Node): ts.Node => + ts.isPropertyAssignment(node) && + ts.isStringLiteralLike(node.name) && + identifierLikeRe.test(node.name.text) + ? ts.factory.createPropertyAssignment( + ts.factory.createIdentifier(node.name.text), + ts.visitNode(node.initializer, visitor) as ts.Expression + ) + : ts.isStringLiteralLike(node) && node.text === '[undefined]' + ? ts.factory.createIdentifier('undefined') + : ts.visitEachChild(node, visitor, context); - return ts.visitNode(node, visitor); - }, - ]); + return ts.visitNode(node, visitor); + }, + ]); - return tsPrinter.printNode(ts.EmitHint.Expression, unquoted.transformed[0], src); + return tsPrinter.printNode(ts.EmitHint.Expression, unquoted.transformed[0], src); }; /** Parses the source file, memoizing the last document so cursor moves are efficient */ const parseSourceFile = memoizeLast((text: string) => - ts.createSourceFile('', text, ts.ScriptTarget.ES5, true) + ts.createSourceFile('', text, ts.ScriptTarget.ES5, true) ); const assertionFailureMessageRe = /^Expected values to be strictly (deep-)?equal:/; /** Gets information about the failing assertion at the poisition, if any. */ function detectFailingDeepStrictEqualAssertion( - document: TextDocument, - position: Position + document: TextDocument, + position: Position ): { assertion: StrictEqualAssertion; actualJSONValue: unknown } | undefined { - const sf = parseSourceFile(document.getText()); - const offset = document.offsetAt(position); - const assertion = StrictEqualAssertion.atPosition(sf, offset); - if (!assertion) { - return undefined; - } + const sf = parseSourceFile(document.getText()); + const offset = document.offsetAt(position); + const assertion = StrictEqualAssertion.atPosition(sf, offset); + if (!assertion) { + return undefined; + } - const startLine = document.positionAt(assertion.offsetStart).line; - const messages = getAllTestStatusMessagesAt(document.uri, startLine); - const strictDeepEqualMessage = messages.find(m => - assertionFailureMessageRe.test(typeof m.message === 'string' ? m.message : m.message.value) - ); + const startLine = document.positionAt(assertion.offsetStart).line; + const messages = getAllTestStatusMessagesAt(document.uri, startLine); + const strictDeepEqualMessage = messages.find(m => + assertionFailureMessageRe.test(typeof m.message === 'string' ? m.message : m.message.value) + ); - if (!strictDeepEqualMessage) { - return undefined; - } + if (!strictDeepEqualMessage) { + return undefined; + } - const metadata = getTestMessageMetadata(strictDeepEqualMessage); - if (!metadata) { - return undefined; - } + const metadata = getTestMessageMetadata(strictDeepEqualMessage); + if (!metadata) { + return undefined; + } - return { - assertion: assertion, - actualJSONValue: metadata.actualValue, - }; + return { + assertion: assertion, + actualJSONValue: metadata.actualValue, + }; } class StrictEqualAssertion { - /** - * Extracts the assertion at the current node, if it is one. - */ - public static fromNode(node: ts.Node): StrictEqualAssertion | undefined { - if (!ts.isCallExpression(node)) { - return undefined; - } + /** + * Extracts the assertion at the current node, if it is one. + */ + public static fromNode(node: ts.Node): StrictEqualAssertion | undefined { + if (!ts.isCallExpression(node)) { + return undefined; + } - const expr = node.expression.getText(); - if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') { - return undefined; - } + const expr = node.expression.getText(); + if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') { + return undefined; + } - return new StrictEqualAssertion(node); - } + return new StrictEqualAssertion(node); + } - /** - * Gets the equals assertion at the given offset in the file. - */ - public static atPosition(sf: ts.SourceFile, offset: number): StrictEqualAssertion | undefined { - let node = findNodeAt(sf, offset); + /** + * Gets the equals assertion at the given offset in the file. + */ + public static atPosition(sf: ts.SourceFile, offset: number): StrictEqualAssertion | undefined { + let node = findNodeAt(sf, offset); - while (node.parent) { - const obj = StrictEqualAssertion.fromNode(node); - if (obj) { - return obj; - } - node = node.parent; - } + while (node.parent) { + const obj = StrictEqualAssertion.fromNode(node); + if (obj) { + return obj; + } + node = node.parent; + } - return undefined; - } + return undefined; + } - constructor(private readonly expression: ts.CallExpression) { } + constructor(private readonly expression: ts.CallExpression) { } - /** Gets the expected value */ - public get expectedValue(): ts.Expression | undefined { - return this.expression.arguments[1]; - } + /** Gets the expected value */ + public get expectedValue(): ts.Expression | undefined { + return this.expression.arguments[1]; + } - /** Gets the position of the assertion expression. */ - public get offsetStart(): number { - return this.expression.getStart(); - } + /** Gets the position of the assertion expression. */ + public get offsetStart(): number { + return this.expression.getStart(); + } } function findNodeAt(parent: ts.Node, offset: number): ts.Node { - for (const child of parent.getChildren()) { - if (child.getStart() <= offset && offset <= child.getEnd()) { - return findNodeAt(child, offset); - } - } - return parent; + for (const child of parent.getChildren()) { + if (child.getStart() <= offset && offset <= child.getEnd()) { + return findNodeAt(child, offset); + } + } + return parent; } function getAllTestStatusMessagesAt(uri: Uri, lineNumber: number): TestMessage[] { - if (tests.testResults.length === 0) { - return []; - } + if (tests.testResults.length === 0) { + return []; + } - const run = tests.testResults[0]; - const snapshots = getTestResultsWithUri(run, uri); - const result: TestMessage[] = []; + const run = tests.testResults[0]; + const snapshots = getTestResultsWithUri(run, uri); + const result: TestMessage[] = []; - for (const snapshot of snapshots) { - for (const m of snapshot.taskStates[0].messages) { - if ( - m.location && - m.location.range.start.line <= lineNumber && - lineNumber <= m.location.range.end.line - ) { - result.push(m); - } - } - } + for (const snapshot of snapshots) { + for (const m of snapshot.taskStates[0].messages) { + if ( + m.location && + m.location.range.start.line <= lineNumber && + lineNumber <= m.location.range.end.line + ) { + result.push(m); + } + } + } - return result; + return result; } function getTestResultsWithUri(testRun: TestRunResult, uri: Uri): TestResultSnapshot[] { - const results: TestResultSnapshot[] = []; + const results: TestResultSnapshot[] = []; - const walk = (r: TestResultSnapshot) => { - for (const c of r.children) { - walk(c); - } - if (r.uri?.toString() === uri.toString()) { - results.push(r); - } - }; + const walk = (r: TestResultSnapshot) => { + for (const c of r.children) { + walk(c); + } + if (r.uri?.toString() === uri.toString()) { + results.push(r); + } + }; - for (const r of testRun.results) { - walk(r); - } + for (const r of testRun.results) { + walk(r); + } - return results; + return results; } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.ts new file mode 100644 index 00000000000..e04d4beede4 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/failureTracker.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 { spawn } from 'child_process'; +import { existsSync, mkdirSync, renameSync } from 'fs'; +import { readFile, writeFile } from 'fs/promises'; +import { dirname, join } from 'path'; +import * as vscode from 'vscode'; + +interface IGitState { + commitId: string; + tracked: string; + untracked: Record; +} + +interface ITrackedRemediation { + snapshot: vscode.TestResultSnapshot; + failing: IGitState; + passing: IGitState; +} + +const MAX_FAILURES = 10; + +export class FailureTracker { + private readonly disposables: vscode.Disposable[] = []; + private readonly lastFailed = new Map< + string, + { snapshot: vscode.TestResultSnapshot; failing: IGitState } + >(); + + private readonly logFile: string; + private logs?: ITrackedRemediation[]; + + constructor(context: vscode.ExtensionContext, private readonly rootDir: string) { + this.logFile = join(context.globalStorageUri.fsPath, '.build/vscode-test-failures.json'); + mkdirSync(dirname(this.logFile), { recursive: true }); + + const oldLogFile = join(rootDir, '.build/vscode-test-failures.json'); + if (existsSync(oldLogFile)) { + try { + renameSync(oldLogFile, this.logFile); + } catch { + // ignore + } + } + + this.disposables.push( + vscode.commands.registerCommand('selfhost-test-provider.openFailureLog', async () => { + const doc = await vscode.workspace.openTextDocument(this.logFile); + await vscode.window.showTextDocument(doc); + }) + ); + + this.disposables.push( + vscode.tests.onDidChangeTestResults(() => { + const last = vscode.tests.testResults[0]; + if (!last) { + return; + } + + let gitState: Promise | undefined; + const getGitState = () => gitState ?? (gitState = this.captureGitState()); + + const queue = [last.results]; + for (let i = 0; i < queue.length; i++) { + for (const snapshot of queue[i]) { + // only interested in states of leaf tests + if (snapshot.children.length) { + queue.push(snapshot.children); + continue; + } + + const key = `${snapshot.uri}/${snapshot.id}`; + const prev = this.lastFailed.get(key); + if (snapshot.taskStates.some(s => s.state === vscode.TestResultState.Failed)) { + // unset the parent to avoid a circular JSON structure: + getGitState().then(s => + this.lastFailed.set(key, { + snapshot: { ...snapshot, parent: undefined }, + failing: s, + }) + ); + } else if (prev) { + this.lastFailed.delete(key); + getGitState().then(s => this.append({ ...prev, passing: s })); + } + } + } + }) + ); + } + + private async append(log: ITrackedRemediation) { + if (!this.logs) { + try { + this.logs = JSON.parse(await readFile(this.logFile, 'utf-8')); + } catch { + this.logs = []; + } + } + + const logs = this.logs!; + logs.push(log); + if (logs.length > MAX_FAILURES) { + logs.splice(0, logs.length - MAX_FAILURES); + } + + await writeFile(this.logFile, JSON.stringify(logs, undefined, 2)); + } + + private async captureGitState() { + const [commitId, tracked, untracked] = await Promise.all([ + this.exec('git', ['rev-parse', 'HEAD']), + this.exec('git', ['diff', 'HEAD']), + this.exec('git', ['ls-files', '--others', '--exclude-standard']).then(async output => { + const mapping: Record = {}; + await Promise.all( + output + .trim() + .split('\n') + .map(async f => { + mapping[f] = await readFile(join(this.rootDir, f), 'utf-8'); + }) + ); + return mapping; + }), + ]); + return { commitId, tracked, untracked }; + } + + public dispose() { + this.disposables.forEach(d => d.dispose()); + } + + private exec(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { stdio: 'pipe', cwd: this.rootDir }); + let output = ''; + child.stdout.setEncoding('utf-8').on('data', b => (output += b)); + child.stderr.setEncoding('utf-8').on('data', b => (output += b)); + child.on('error', reject); + child.on('exit', code => + code === 0 + ? resolve(output) + : reject(new Error(`Failed with error code ${code}\n${output}`)) + ); + }); + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts index 52c2aa2c98f..df6c2e77ed2 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/memoize.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ export const memoizeLast = (fn: (args: A) => T): ((args: A) => T) => { - let last: { arg: A; result: T } | undefined; - return arg => { - if (last && last.arg === arg) { - return last.result; - } + let last: { arg: A; result: T } | undefined; + return arg => { + if (last && last.arg === arg) { + return last.result; + } - const result = fn(arg); - last = { arg, result }; - return result; - }; + const result = fn(arg); + last = { arg, result }; + return result; + }; }; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts index 08540b14fbf..8b44c52b72f 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/metadata.ts @@ -5,8 +5,8 @@ import { TestMessage } from 'vscode'; export interface TestMessageMetadata { - expectedValue: unknown; - actualValue: unknown; + expectedValue: unknown; + actualValue: unknown; } const cache = new Array<{ id: string; metadata: TestMessageMetadata }>(); @@ -14,48 +14,48 @@ const cache = new Array<{ id: string; metadata: TestMessageMetadata }>(); let id = 0; function getId(): string { - return `msg:${id++}:`; + return `msg:${id++}:`; } const regexp = /msg:\d+:/; export function attachTestMessageMetadata( - message: TestMessage, - metadata: TestMessageMetadata + message: TestMessage, + metadata: TestMessageMetadata ): void { - const existingMetadata = getTestMessageMetadata(message); - if (existingMetadata) { - Object.assign(existingMetadata, metadata); - return; - } + const existingMetadata = getTestMessageMetadata(message); + if (existingMetadata) { + Object.assign(existingMetadata, metadata); + return; + } - const id = getId(); + const id = getId(); - if (typeof message.message === 'string') { - message.message = `${message.message}\n${id}`; - } else { - message.message.appendText(`\n${id}`); - } + if (typeof message.message === 'string') { + message.message = `${message.message}\n${id}`; + } else { + message.message.appendText(`\n${id}`); + } - cache.push({ id, metadata }); - while (cache.length > 100) { - cache.shift(); - } + cache.push({ id, metadata }); + while (cache.length > 100) { + cache.shift(); + } } export function getTestMessageMetadata(message: TestMessage): TestMessageMetadata | undefined { - let value: string; - if (typeof message.message === 'string') { - value = message.message; - } else { - value = message.message.value; - } + let value: string; + if (typeof message.message === 'string') { + value = message.message; + } else { + value = message.message.value; + } - const result = regexp.exec(value); - if (!result) { - return undefined; - } + const result = regexp.exec(value); + if (!result) { + return undefined; + } - const id = result[0]; - return cache.find(c => c.id === id)?.metadata; + const id = result[0]; + return cache.find(c => c.id === id)?.metadata; } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts index 33fbc8fa8bb..5b3a624617e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/snapshot.ts @@ -9,15 +9,15 @@ import * as vscode from 'vscode'; export const snapshotComment = '\n\n// Snapshot file: '; export const registerSnapshotUpdate = (ctrl: vscode.TestController) => - vscode.commands.registerCommand('selfhost-test-provider.updateSnapshot', async args => { - const message: vscode.TestMessage = args.message; - const index = message.expectedOutput?.indexOf(snapshotComment); - if (!message.expectedOutput || !message.actualOutput || !index || index === -1) { - vscode.window.showErrorMessage('Could not find snapshot comment in message'); - return; - } + vscode.commands.registerCommand('selfhost-test-provider.updateSnapshot', async args => { + const message: vscode.TestMessage = args.message; + const index = message.expectedOutput?.indexOf(snapshotComment); + if (!message.expectedOutput || !message.actualOutput || !index || index === -1) { + vscode.window.showErrorMessage('Could not find snapshot comment in message'); + return; + } - const file = message.expectedOutput.slice(index + snapshotComment.length); - await fs.writeFile(file, message.actualOutput); - ctrl.invalidateTestResults(args.test); - }); + const file = message.expectedOutput.slice(index + snapshotComment.length); + await fs.writeFile(file, message.actualOutput); + ctrl.invalidateTestResults(args.test); + }); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts index 944d3bc6f6d..56b26cafda8 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts @@ -10,59 +10,59 @@ import { TestCase, TestConstruct, TestSuite, VSCodeTest } from './testTree'; const suiteNames = new Set(['suite', 'flakySuite']); export const enum Action { - Skip, - Recurse, + Skip, + Recurse, } export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: VSCodeTest) => { - if (!ts.isCallExpression(node)) { - return Action.Recurse; - } + if (!ts.isCallExpression(node)) { + return Action.Recurse; + } - let lhs = node.expression; - if (isSkipCall(lhs)) { - return Action.Skip; - } + let lhs = node.expression; + if (isSkipCall(lhs)) { + return Action.Skip; + } - if (isPropertyCall(lhs) && lhs.name.text === 'only') { - lhs = lhs.expression; - } + if (isPropertyCall(lhs) && lhs.name.text === 'only') { + lhs = lhs.expression; + } - const name = node.arguments[0]; - const func = node.arguments[1]; - if (!name || !ts.isIdentifier(lhs) || !ts.isStringLiteralLike(name)) { - return Action.Recurse; - } + const name = node.arguments[0]; + const func = node.arguments[1]; + if (!name || !ts.isIdentifier(lhs) || !ts.isStringLiteralLike(name)) { + return Action.Recurse; + } - if (!func) { - return Action.Recurse; - } + if (!func) { + return Action.Recurse; + } - const start = src.getLineAndCharacterOfPosition(name.pos); - const end = src.getLineAndCharacterOfPosition(func.end); - const range = new vscode.Range( - new vscode.Position(start.line, start.character), - new vscode.Position(end.line, end.character) - ); + const start = src.getLineAndCharacterOfPosition(name.pos); + const end = src.getLineAndCharacterOfPosition(func.end); + const range = new vscode.Range( + new vscode.Position(start.line, start.character), + new vscode.Position(end.line, end.character) + ); - const cparent = parent instanceof TestConstruct ? parent : undefined; - if (lhs.escapedText === 'test') { - return new TestCase(name.text, range, cparent); - } + const cparent = parent instanceof TestConstruct ? parent : undefined; + if (lhs.escapedText === 'test') { + return new TestCase(name.text, range, cparent); + } - if (suiteNames.has(lhs.escapedText.toString())) { - return new TestSuite(name.text, range, cparent); - } + if (suiteNames.has(lhs.escapedText.toString())) { + return new TestSuite(name.text, range, cparent); + } - return Action.Recurse; + return Action.Recurse; }; const isPropertyCall = ( - lhs: ts.LeftHandSideExpression + lhs: ts.LeftHandSideExpression ): lhs is ts.PropertyAccessExpression & { expression: ts.Identifier; name: ts.Identifier } => - ts.isPropertyAccessExpression(lhs) && - ts.isIdentifier(lhs.expression) && - ts.isIdentifier(lhs.name); + ts.isPropertyAccessExpression(lhs) && + ts.isIdentifier(lhs.expression) && + ts.isIdentifier(lhs.name); const isSkipCall = (lhs: ts.LeftHandSideExpression) => - isPropertyCall(lhs) && suiteNames.has(lhs.expression.text) && lhs.name.text === 'skip'; + isPropertyCall(lhs) && suiteNames.has(lhs.expression.text) && lhs.name.text === 'skip'; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts index fd28b3772da..bb5bc5f28dd 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/streamSplitter.ts @@ -28,7 +28,11 @@ export class StreamSplitter extends Transform { } } - override _transform(chunk: Buffer, _encoding: string, callback: (error?: Error | null, data?: any) => void): void { + override _transform( + chunk: Buffer, + _encoding: string, + callback: (error?: Error | null, data?: any) => void + ): void { if (!this.buffer) { this.buffer = chunk; } else { diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts index 39da7213325..74013dcd561 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testOutputScanner.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { + decodedMappings, GREATEST_LOWER_BOUND, LEAST_UPPER_BOUND, originalPositionFor, @@ -12,11 +13,12 @@ import { import * as styles from 'ansi-styles'; import { ChildProcessWithoutNullStreams } from 'child_process'; import * as vscode from 'vscode'; -import { coverageContext } from './coverageProvider'; +import { istanbulCoverageContext, PerTestCoverageTracker } from './coverageProvider'; import { attachTestMessageMetadata } from './metadata'; import { snapshotComment } from './snapshot'; -import { getContentFromFilesystem } from './testTree'; import { StreamSplitter } from './streamSplitter'; +import { getContentFromFilesystem } from './testTree'; +import { IScriptCoverage } from './v8CoverageWrangling'; export const enum MochaEvent { Start = 'start', @@ -24,6 +26,10 @@ export const enum MochaEvent { Pass = 'pass', Fail = 'fail', End = 'end', + + // custom events: + CoverageInit = 'coverageInit', + CoverageIncrement = 'coverageIncrement', } export interface IStartEvent { @@ -62,12 +68,20 @@ export interface IEndEvent { end: string /* ISO date */; } +export interface ITestCoverageCoverage { + file: string; + fullTitle: string; + coverage: { result: IScriptCoverage[] }; +} + export type MochaEventTuple = | [MochaEvent.Start, IStartEvent] | [MochaEvent.TestStart, ITestStartEvent] | [MochaEvent.Pass, IPassEvent] | [MochaEvent.Fail, IFailEvent] - | [MochaEvent.End, IEndEvent]; + | [MochaEvent.End, IEndEvent] + | [MochaEvent.CoverageInit, { result: IScriptCoverage[] }] + | [MochaEvent.CoverageIncrement, ITestCoverageCoverage]; const LF = '\n'.charCodeAt(0); @@ -160,6 +174,7 @@ export async function scanTestOutput( return prom; }; + let perTestCoverage: PerTestCoverageTracker | undefined; let lastTest: vscode.TestItem | undefined; let ranAnyTest = false; @@ -189,7 +204,7 @@ export async function scanTestOutput( return; } - const logLocation = store.getSourceLocation(match[2], Number(match[3])); + const logLocation = store.getSourceLocation(match[2], Number(match[3]) - 1); const logContents = replaceAllLocations(store, match[1]); const test = currentTest; @@ -224,7 +239,6 @@ export async function scanTestOutput( if (tcase) { lastTest = tcase; task.passed(tcase, evt[1].duration); - tests.delete(title); } } break; @@ -258,8 +272,6 @@ export async function scanTestOutput( return; } - tests.delete(id); - const hasDiff = actual !== undefined && expected !== undefined && @@ -307,15 +319,36 @@ export async function scanTestOutput( case MochaEvent.End: // no-op, we wait until the process exits to ensure coverage is written out break; + case MochaEvent.CoverageInit: + perTestCoverage ??= new PerTestCoverageTracker(store); + for (const result of evt[1].result) { + perTestCoverage.add(result); + } + break; + case MochaEvent.CoverageIncrement: { + const { fullTitle, coverage } = evt[1]; + const tcase = tests.get(fullTitle); + if (tcase) { + perTestCoverage ??= new PerTestCoverageTracker(store); + for (const result of coverage.result) { + perTestCoverage.add(result, tcase); + } + } + break; + } } }); }); + if (perTestCoverage) { + enqueueExitBlocker(perTestCoverage.report(task)); + } + await Promise.all([...exitBlockers]); if (coverageDir) { try { - await coverageContext.apply(task, coverageDir, { + await istanbulCoverageContext.apply(task, coverageDir, { mapFileUri: uri => store.getSourceFile(uri.toString()), mapLocation: (uri, position) => store.getSourceLocation(uri.toString(), position.line, position.character), @@ -391,26 +424,44 @@ 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 class SourceMapStore { private readonly cache = new Map>(); - async getSourceLocation(fileUri: string, line: number, col = 1) { + async getSourceLocationMapper(fileUri: string) { const sourceMap = await this.loadSourceMap(fileUri); - if (!sourceMap) { - return undefined; - } - - for (const bias of sourceMapBiases) { - const position = originalPositionFor(sourceMap, { column: col, line: line + 1, 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 (line: number, col: number) => { + if (!sourceMap) { + return undefined; } - } - 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; + }; + } + + /** 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)); } async getSourceFile(compiledUri: string) { @@ -549,5 +600,5 @@ async function tryDeriveStackLocation( async function deriveSourceLocation(store: SourceMapStore, parts: RegExpMatchArray) { const [, fileUri, line, col] = parts; - return store.getSourceLocation(fileUri, Number(line), Number(col)); + return store.getSourceLocation(fileUri, Number(line) - 1, Number(col)); } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts index 6ecfb8bc07f..7a54c5c0d32 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts @@ -22,155 +22,155 @@ export const clearFileDiagnostics = (uri: vscode.Uri) => diagnosticCollection.de * Tries to guess which workspace folder VS Code is in. */ export const guessWorkspaceFolder = async () => { - if (!vscode.workspace.workspaceFolders) { - return undefined; - } + if (!vscode.workspace.workspaceFolders) { + return undefined; + } - if (vscode.workspace.workspaceFolders.length < 2) { - return vscode.workspace.workspaceFolders[0]; - } + if (vscode.workspace.workspaceFolders.length < 2) { + return vscode.workspace.workspaceFolders[0]; + } - for (const folder of vscode.workspace.workspaceFolders) { - try { - await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js')); - return folder; - } catch { - // ignored - } - } + for (const folder of vscode.workspace.workspaceFolders) { + try { + await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js')); + return folder; + } catch { + // ignored + } + } - return undefined; + return undefined; }; export const getContentFromFilesystem: ContentGetter = async uri => { - try { - const rawContent = await vscode.workspace.fs.readFile(uri); - return textDecoder.decode(rawContent); - } catch (e) { - console.warn(`Error providing tests for ${uri.fsPath}`, e); - return ''; - } + try { + const rawContent = await vscode.workspace.fs.readFile(uri); + return textDecoder.decode(rawContent); + } catch (e) { + console.warn(`Error providing tests for ${uri.fsPath}`, e); + return ''; + } }; export class TestFile { - public hasBeenRead = false; + public hasBeenRead = false; - constructor( - public readonly uri: vscode.Uri, - public readonly workspaceFolder: vscode.WorkspaceFolder - ) { } + constructor( + public readonly uri: vscode.Uri, + public readonly workspaceFolder: vscode.WorkspaceFolder + ) {} - public getId() { - return this.uri.toString().toLowerCase(); - } + public getId() { + return this.uri.toString().toLowerCase(); + } - public getLabel() { - return relative(join(this.workspaceFolder.uri.fsPath, 'src'), this.uri.fsPath); - } + public getLabel() { + return relative(join(this.workspaceFolder.uri.fsPath, 'src'), this.uri.fsPath); + } - public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) { - try { - const content = await getContentFromFilesystem(item.uri!); - item.error = undefined; - this.updateFromContents(controller, content, item); - } catch (e) { - item.error = (e as Error).stack; - } - } + public async updateFromDisk(controller: vscode.TestController, item: vscode.TestItem) { + try { + const content = await getContentFromFilesystem(item.uri!); + item.error = undefined; + this.updateFromContents(controller, content, item); + } catch (e) { + item.error = (e as Error).stack; + } + } - /** - * Refreshes all tests in this file, `sourceReader` provided by the root. - */ - public updateFromContents( - controller: vscode.TestController, - content: string, - file: vscode.TestItem - ) { - try { - const diagnostics: vscode.Diagnostic[] = []; - const ast = ts.createSourceFile( - this.uri.path.split('/').pop()!, - content, - ts.ScriptTarget.ESNext, - false, - ts.ScriptKind.TS - ); + /** + * Refreshes all tests in this file, `sourceReader` provided by the root. + */ + public updateFromContents( + controller: vscode.TestController, + content: string, + file: vscode.TestItem + ) { + try { + const diagnostics: vscode.Diagnostic[] = []; + const ast = ts.createSourceFile( + this.uri.path.split('/').pop()!, + content, + ts.ScriptTarget.ESNext, + false, + ts.ScriptKind.TS + ); - const parents: { item: vscode.TestItem; children: vscode.TestItem[] }[] = [ - { item: file, children: [] }, - ]; - const traverse = (node: ts.Node) => { - const parent = parents[parents.length - 1]; - const childData = extractTestFromNode(ast, node, itemData.get(parent.item)!); - if (childData === Action.Skip) { - return; - } + const parents: { item: vscode.TestItem; children: vscode.TestItem[] }[] = [ + { item: file, children: [] }, + ]; + const traverse = (node: ts.Node) => { + const parent = parents[parents.length - 1]; + const childData = extractTestFromNode(ast, node, itemData.get(parent.item)!); + if (childData === Action.Skip) { + return; + } - if (childData === Action.Recurse) { - ts.forEachChild(node, traverse); - return; - } + if (childData === Action.Recurse) { + ts.forEachChild(node, traverse); + return; + } - const id = `${file.uri}/${childData.fullName}`.toLowerCase(); + const id = `${file.uri}/${childData.fullName}`.toLowerCase(); - // Skip duplicated tests. They won't run correctly with the way - // mocha reports them, and will error if we try to insert them. - const existing = parent.children.find(c => c.id === id); - if (existing) { - const diagnostic = new vscode.Diagnostic( - childData.range, - 'Duplicate tests cannot be run individually and will not be reported correctly by the test framework. Please rename them.', - vscode.DiagnosticSeverity.Warning - ); + // Skip duplicated tests. They won't run correctly with the way + // mocha reports them, and will error if we try to insert them. + const existing = parent.children.find(c => c.id === id); + if (existing) { + const diagnostic = new vscode.Diagnostic( + childData.range, + 'Duplicate tests cannot be run individually and will not be reported correctly by the test framework. Please rename them.', + vscode.DiagnosticSeverity.Warning + ); - diagnostic.relatedInformation = [ - new vscode.DiagnosticRelatedInformation( - new vscode.Location(existing.uri!, existing.range!), - 'First declared here' - ), - ]; + diagnostic.relatedInformation = [ + new vscode.DiagnosticRelatedInformation( + new vscode.Location(existing.uri!, existing.range!), + 'First declared here' + ), + ]; - diagnostics.push(diagnostic); - return; - } + diagnostics.push(diagnostic); + return; + } - const item = controller.createTestItem(id, childData.name, file.uri); - itemData.set(item, childData); - item.range = childData.range; - parent.children.push(item); + const item = controller.createTestItem(id, childData.name, file.uri); + itemData.set(item, childData); + item.range = childData.range; + parent.children.push(item); - if (childData instanceof TestSuite) { - parents.push({ item: item, children: [] }); - ts.forEachChild(node, traverse); - item.children.replace(parents.pop()!.children); - } - }; + if (childData instanceof TestSuite) { + parents.push({ item: item, children: [] }); + ts.forEachChild(node, traverse); + item.children.replace(parents.pop()!.children); + } + }; - ts.forEachChild(ast, traverse); - file.error = undefined; - file.children.replace(parents[0].children); - diagnosticCollection.set(this.uri, diagnostics.length ? diagnostics : undefined); - this.hasBeenRead = true; - } catch (e) { - file.error = String((e as Error).stack || (e as Error).message); - } - } + ts.forEachChild(ast, traverse); + file.error = undefined; + file.children.replace(parents[0].children); + diagnosticCollection.set(this.uri, diagnostics.length ? diagnostics : undefined); + this.hasBeenRead = true; + } catch (e) { + file.error = String((e as Error).stack || (e as Error).message); + } + } } export abstract class TestConstruct { - public fullName: string; + public fullName: string; - constructor( - public readonly name: string, - public readonly range: vscode.Range, - parent?: TestConstruct - ) { - this.fullName = parent ? `${parent.fullName} ${name}` : name; - } + constructor( + public readonly name: string, + public readonly range: vscode.Range, + parent?: TestConstruct + ) { + this.fullName = parent ? `${parent.fullName} ${name}` : name; + } } -export class TestSuite extends TestConstruct { } +export class TestSuite extends TestConstruct {} -export class TestCase extends TestConstruct { } +export class TestCase extends TestConstruct {} export type VSCodeTest = TestFile | TestSuite | TestCase; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.ts new file mode 100644 index 00000000000..c2564ca61c3 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.test.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 assert from 'assert'; +import { RangeCoverageTracker } from './v8CoverageWrangling'; + +suite('v8CoverageWrangling', () => { + suite('RangeCoverageTracker', () => { + test('covers new range', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]); + }); + + test('non overlapping ranges', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.cover(15, 20); + rt.cover(12, 13); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 10, covered: true }, + { start: 12, end: 13, covered: true }, + { start: 15, end: 20, covered: true }, + ] + ); + }); + + test('covers exact', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(5, 10); + assert.deepStrictEqual([...rt], [{ start: 5, end: 10, covered: true }]); + }); + + test('overlap at start', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(2, 7); + assert.deepStrictEqual( + [...rt], + [ + { start: 2, end: 7, covered: true }, + { start: 7, end: 10, covered: false }, + ] + ); + }); + + test('overlap at end', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.uncovered(2, 7); + assert.deepStrictEqual( + [...rt], + [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 10, covered: true }, + ] + ); + }); + + test('inner contained', () => { + const rt = new RangeCoverageTracker(); + rt.cover(5, 10); + rt.uncovered(2, 12); + assert.deepStrictEqual( + [...rt], + [ + { start: 2, end: 5, covered: false }, + { start: 5, end: 10, covered: true }, + { start: 10, end: 12, covered: false }, + ] + ); + }); + + test('outer contained', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(7, 9); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 7, covered: false }, + { start: 7, end: 9, covered: true }, + { start: 9, end: 10, covered: false }, + ] + ); + }); + + test('boundary touching', () => { + const rt = new RangeCoverageTracker(); + rt.uncovered(5, 10); + rt.cover(10, 15); + rt.uncovered(15, 20); + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 10, covered: false }, + { start: 10, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + ] + ); + }); + + suite('initializeBlock', () => { + test('simple tree', () => { + const rt = RangeCoverageTracker.initializeBlocks([ + { + functionName: 'outer', + isBlockCoverage: true, + ranges: [ + { count: 1, startOffset: 5, endOffset: 30 }, + { count: 1, startOffset: 8, endOffset: 10 }, + { count: 0, startOffset: 15, endOffset: 20 }, + ], + }, + ]); + + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 15, covered: true }, + { start: 15, end: 20, covered: false }, + { start: 20, end: 30, covered: true }, + ] + ); + }); + + test('separate branches', () => { + const rt = RangeCoverageTracker.initializeBlocks([ + { + functionName: 'outer', + isBlockCoverage: true, + ranges: [ + { count: 1, startOffset: 5, endOffset: 8 }, + { count: 1, startOffset: 10, endOffset: 12 }, + { count: 0, startOffset: 15, endOffset: 20 }, + ], + }, + ]); + + assert.deepStrictEqual( + [...rt], + [ + { start: 5, end: 8, covered: true }, + { start: 10, end: 12, covered: true }, + { start: 15, end: 20, covered: false }, + ] + ); + }); + }); + }); +}); diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.ts new file mode 100644 index 00000000000..ede638430ba --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/v8CoverageWrangling.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. + *--------------------------------------------------------------------------------------------*/ + +export interface ICoverageRange { + start: number; + end: number; + covered: boolean; +} + +export interface IV8FunctionCoverage { + functionName: string; + isBlockCoverage: boolean; + ranges: IV8CoverageRange[]; +} + +export interface IV8CoverageRange { + startOffset: number; + endOffset: number; + count: number; +} + +/** V8 Script coverage data */ +export interface IScriptCoverage { + scriptId: string; + url: string; + // Script source added by the runner the first time the script is emitted. + source?: string; + functions: IV8FunctionCoverage[]; +} + +export class RangeCoverageTracker implements Iterable { + /** + * A noncontiguous, non-overlapping, ordered set of ranges and whether + * that range has been covered. + */ + private ranges: readonly ICoverageRange[] = []; + + /** + * Adds a coverage tracker initialized for a function with {@link isBlockCoverage} set to true. + */ + public static initializeBlocks(fns: IV8FunctionCoverage[]) { + const rt = new RangeCoverageTracker(); + + let start = 0; + const stack: IV8CoverageRange[] = []; + + // note: comes pre-sorted from V8 + for (const { ranges } of fns) { + for (const range of ranges) { + while (stack.length && stack[stack.length - 1].endOffset < range.startOffset) { + const last = stack.pop()!; + rt.setCovered(start, last.endOffset, last.count > 0); + start = last.endOffset; + } + + if (range.startOffset > start && stack.length) { + rt.setCovered(start, range.startOffset, !!stack[stack.length - 1].count); + } + + start = range.startOffset; + stack.push(range); + } + } + + while (stack.length) { + const last = stack.pop()!; + rt.setCovered(start, last.endOffset, last.count > 0); + start = last.endOffset; + } + + return rt; + } + + /** Makes a copy of the range tracker. */ + public clone() { + const rt = new RangeCoverageTracker(); + rt.ranges = this.ranges.slice(); + return rt; + } + + /** Marks a range covered */ + public cover(start: number, end: number) { + this.setCovered(start, end, true); + } + + /** Marks a range as uncovered */ + public uncovered(start: number, end: number) { + this.setCovered(start, end, false); + } + + /** Iterates over coverage ranges */ + [Symbol.iterator]() { + return this.ranges[Symbol.iterator](); + } + + /** + * Marks the given character range as being covered or uncovered. + * + * todo@connor4312: this is a hot path is could probably be optimized to + * avoid rebuilding the array. Maybe with a nice tree structure? + */ + public setCovered(start: number, end: number, covered: boolean) { + const newRanges: ICoverageRange[] = []; + let i = 0; + for (; i < this.ranges.length && this.ranges[i].end <= start; i++) { + newRanges.push(this.ranges[i]); + } + + const push = (range: ICoverageRange) => { + const last = newRanges.length && newRanges[newRanges.length - 1]; + if (last && last.end === range.start && last.covered === range.covered) { + last.end = range.end; + } else { + newRanges.push(range); + } + }; + + push({ start, end, covered }); + + for (; i < this.ranges.length; i++) { + const range = this.ranges[i]; + const last = newRanges[newRanges.length - 1]; + + if (range.start === last.start && range.end === last.end) { + // ranges are equal: + last.covered ||= range.covered; + } else if (range.end < last.start || range.start > last.end) { + // ranges don't overlap + push(range); + } else if (range.start < last.start && range.end > last.end) { + // range contains last: + newRanges.pop(); + push({ start: range.start, end: last.start, covered: range.covered }); + push({ start: last.start, end: last.end, covered: range.covered || last.covered }); + push({ start: last.end, end: range.end, covered: range.covered }); + } else if (range.start >= last.start && range.end <= last.end) { + // last contains range: + newRanges.pop(); + push({ start: last.start, end: range.start, covered: last.covered }); + push({ start: range.start, end: range.end, covered: range.covered || last.covered }); + push({ start: range.end, end: last.end, covered: last.covered }); + } else if (range.start < last.start && range.end <= last.end) { + // range overlaps start of last: + newRanges.pop(); + push({ start: range.start, end: last.start, covered: range.covered }); + push({ start: last.start, end: range.end, covered: range.covered || last.covered }); + push({ start: range.end, end: last.end, covered: last.covered }); + } else if (range.start >= last.start && range.end > last.end) { + // range overlaps end of last: + newRanges.pop(); + push({ start: last.start, end: range.start, covered: last.covered }); + push({ start: range.start, end: last.end, covered: range.covered || last.covered }); + push({ start: last.end, end: range.end, covered: range.covered }); + } else { + throw new Error('unreachable'); + } + } + + this.ranges = newRanges; + } +} + +export class OffsetToPosition { + /** Line numbers to byte offsets. */ + public readonly lines: number[] = []; + + public readonly totalLength: number; + + constructor(source: string) { + this.lines.push(0); + for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) { + this.lines.push(i + 1); + } + this.totalLength = source.length; + } + + public getLineLength(lineNumber: number): number { + return ( + (lineNumber < this.lines.length - 1 ? this.lines[lineNumber + 1] - 1 : this.totalLength) - + this.lines[lineNumber] + ); + } + + /** + * Gets the line the offset appears on. + */ + public getLineOfOffset(offset: number): number { + let low = 0; + let high = this.lines.length; + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (this.lines[mid] > offset) { + high = mid; + } else { + low = mid + 1; + } + } + + return low - 1; + } + + /** + * Converts from a file offset to a base 0 line/column . + */ + public toLineColumn(offset: number): { line: number; column: number } { + const line = this.getLineOfOffset(offset); + return { line: line, column: offset - this.lines[line] }; + } +} diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts index e8c36118c65..5d73928aed5 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/vscodeTestRunner.ts @@ -24,7 +24,7 @@ const ATTACH_CONFIG_NAME = 'Attach to VS Code'; const DEBUG_TYPE = 'pwa-chrome'; export abstract class VSCodeTestRunner { - constructor(protected readonly repoLocation: vscode.WorkspaceFolder) { } + constructor(protected readonly repoLocation: vscode.WorkspaceFolder) {} public async run(baseArgs: ReadonlyArray, filter?: ReadonlyArray) { const args = this.prepareArguments(baseArgs, filter); @@ -303,5 +303,5 @@ export const PlatformTestRunner = process.platform === 'win32' ? WindowsTestRunner : process.platform === 'darwin' - ? DarwinTestRunner - : PosixTestRunner; + ? DarwinTestRunner + : PosixTestRunner; diff --git a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json index 4bc025b62ba..0183a2ff57e 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json @@ -3,12 +3,14 @@ "compilerOptions": { "outDir": "./out", "types": [ - "node" + "node", + "mocha", ] }, "include": [ "src/**/*", "../../../src/vscode-dts/vscode.d.ts", "../../../src/vscode-dts/vscode.proposed.testObserver.d.ts", + "../../../src/vscode-dts/vscode.proposed.attributableCoverage.d.ts", ] } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock index bf2295ed7b3..50478f52c73 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock +++ b/.vscode/extensions/vscode-selfhost-test-provider/yarn.lock @@ -25,10 +25,15 @@ resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== -"@types/node@18.x": - version "18.19.26" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.26.tgz#18991279d0a0e53675285e8cf4a0823766349729" - integrity sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw== +"@types/mocha@^10.0.6": + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" + integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== + +"@types/node@20.x": + version "20.12.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.11.tgz#c4ef00d3507000d17690643278a60dc55a9dc9be" + integrity sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw== dependencies: undici-types "~5.26.4" diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 4fb94c29edd..d7836922bad 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:\"April 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\n\n$MILESTONE=milestone:\"May 2024\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 3e48f05b3de..3bf56fffce4 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:\"April 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:\"May 2024\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/.yarnrc b/.yarnrc index 616968bddff..b40fb7e7f58 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "28.2.8" -ms_build_id "27744544" +target "29.4.0" +ms_build_id "9593362" runtime "electron" build_from_source "true" diff --git a/build/.cachesalt b/build/.cachesalt index 8051d84124e..a454f1220da 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2024-03-18T08:47:22.277Z +2024-05-16T14:24:05.381Z diff --git a/build/.moduleignore b/build/.moduleignore index e40224556c6..32fb3bd21c5 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -20,6 +20,15 @@ fsevents/test/** @vscode/spdlog/*.yml !@vscode/spdlog/build/Release/*.node +@vscode/deviceid/binding.gyp +@vscode/deviceid/build/** +@vscode/deviceid/deps/** +@vscode/deviceid/src/** +@vscode/deviceid/test/** +@vscode/deviceid/*.yml +!@vscode/deviceid/build/Release/*.node + + @vscode/sqlite3/binding.gyp @vscode/sqlite3/benchmark/** @vscode/sqlite3/cloudformation/** diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 11aa7605f63..8b4bda1c6a2 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -78,8 +78,6 @@ steps: - script: | set -e - export npm_config_arch=$(VSCODE_ARCH) - npm i -g node-gyp@9.4.0 python3 -m pip install setuptools for i in {1..5}; do # try 5 times @@ -91,6 +89,7 @@ steps: echo "Yarn failed $i, trying again..." done env: + npm_config_arch: $(VSCODE_ARCH) ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index f5c00aa0cf0..4968b9ff04f 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -100,6 +100,9 @@ steps: timeoutInMinutes: 20 - script: ./scripts/test-remote-integration.sh + env: + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x + UV_USE_IO_URING: 0 displayName: Run integration tests (Remote) timeoutInMinutes: 20 @@ -134,6 +137,8 @@ steps: ./scripts/test-remote-integration.sh env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH) + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x + UV_USE_IO_URING: 0 displayName: Run integration tests (Remote) timeoutInMinutes: 20 @@ -164,10 +169,16 @@ steps: - script: yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" timeoutInMinutes: 20 + env: + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x + UV_USE_IO_URING: 0 displayName: Run smoke tests (Browser, Chromium) - script: yarn smoketest-no-compile --remote --tracing timeoutInMinutes: 20 + env: + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x + UV_USE_IO_URING: 0 displayName: Run smoke tests (Remote) - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: @@ -178,6 +189,8 @@ steps: - script: yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" env: VSCODE_REMOTE_SERVER_PATH: $(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)-web + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x + UV_USE_IO_URING: 0 timeoutInMinutes: 20 displayName: Run smoke tests (Browser, Chromium) @@ -188,6 +201,9 @@ steps: VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-server-linux-$(VSCODE_ARCH)" \ yarn smoketest-no-compile --tracing --remote --build "$APP_PATH" timeoutInMinutes: 20 + env: + # TODO(deepak1556): Remove this once we update to Node.js >= 20.11.x + UV_USE_IO_URING: 0 displayName: Run smoke tests (Remote) - script: | diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index cdc687fe7ac..352b31360f8 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -100,50 +100,48 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Authentication + - 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 + + source ./build/azure-pipelines/linux/setup-env.sh + + 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: + npm_config_arch: $(NPM_ARCH) + VSCODE_ARCH: $(VSCODE_ARCH) + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + 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: | - set -e - # To workaround the issue of yarn not respecting the registry value from .npmrc - yarn config set registry "$NPM_REGISTRY" - - 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 - - source ./build/azure-pipelines/linux/setup-env.sh - - 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: - npm_config_arch: $(NPM_ARCH) - VSCODE_ARCH: $(VSCODE_ARCH) - NPM_REGISTRY: "$(NPM_REGISTRY)" - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install dependencies (non-OSS) - 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 - - script: node build/azure-pipelines/distro/mixin-npm condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules @@ -154,23 +152,12 @@ steps: - script: | set -e - 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 - cd node_modules/native-keymap && npx node-gyp@9.4.0 -y rebuild --debug cd ../.. && ./.github/workflows/check-clean-git-state.sh env: npm_config_arch: $(NPM_ARCH) - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install dependencies (OSS) + displayName: Rebuild debug version of native modules (OSS) condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index e42a6b12b1f..9bfbf9ab41a 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/120.0.6099.268/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + 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 # Download libcxx headers and objects from upstream electron releases DEBUG=libcxx-fetcher \ @@ -25,12 +25,12 @@ 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/120.0.6099.268:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/120.0.6099.268:build/config/c++/BUILD.gn + # 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 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 --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + 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" export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" # Set compiler toolchain for remote server diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 5fd12caf017..41c33f3f265 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -104,11 +104,13 @@ steps: - script: yarn npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" + DISABLE_V8_COMPILE_CACHE: 1 # Disable v8 cache used by yarn v1.x, refs https://github.com/nodejs/node/issues/51555 displayName: Compile & Hygiene (OSS) - ${{ else }}: - script: yarn npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" + DISABLE_V8_COMPILE_CACHE: 1 # Disable v8 cache used by yarn v1.x, refs https://github.com/nodejs/node/issues/51555 displayName: Compile & Hygiene (non-OSS) - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 72ded6bcc11..bf43d9212cf 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -104,6 +104,7 @@ steps: tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-web echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" env: + DISABLE_V8_COMPILE_CACHE: 1 # Disable v8 cache used by yarn v1.x, refs https://github.com/nodejs/node/issues/51555 GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 3c92499b2a6..d3827b930f8 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -93,16 +93,10 @@ steps: . 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 = "$(VSCODE_ARCH)" - $env:CHILD_CONCURRENCY="1" retry { exec { yarn --frozen-lockfile --check-files } } env: + npm_config_arch: $(VSCODE_ARCH) + CHILD_CONCURRENCY: 1 ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 GITHUB_TOKEN: "$(github-distro-mixin-password)" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 86f78d0adea..a80aa1531f1 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -69b40637a88ad4c17877b3d665b39ad0e11928aa71b19ef45f5b76250d1c9786 *chromedriver-v28.2.8-darwin-arm64.zip -3a9ce6179228245f2c7878c4238e10d51c77dc20642922a226ccc235a20f5a29 *chromedriver-v28.2.8-darwin-x64.zip -7f6470ea5d86dbe68fcc3fccfefd3b7135ba3468ef54b0235bf57cedeabf433d *chromedriver-v28.2.8-linux-arm64.zip -4bfe709d58b237f5c5a7618b2abecf533dac9415d327e763ad6cf622218517cc *chromedriver-v28.2.8-linux-armv7l.zip -7558ee413f96f88b9b9ad5787dd433adcfaf56411fdf052826d39d204ebaba9d *chromedriver-v28.2.8-linux-x64.zip -9814583b075d969c32afb6e929b4bf7956b0223fded996c91341388b8f638dd6 *chromedriver-v28.2.8-mas-arm64.zip -82d11c6606db9aea355b1e410083c72bd1e39abb9e34a839c16b16b75364ea0d *chromedriver-v28.2.8-mas-x64.zip -4803a5335a40ba208136094f5adfde2c4272761d34e0e9e9f4febc2ef676c3ad *chromedriver-v28.2.8-win32-arm64.zip -7b079f47869f7e96a5829f6fb7eff032394f76218b39a2aaf73cc93ce8a68050 *chromedriver-v28.2.8-win32-ia32.zip -2aedd176d4f72b29cd1914364e813756d52f53558df32e3429996b820edc994d *chromedriver-v28.2.8-win32-x64.zip -ae1a521aa36053a3b60b318d7bc093ec7579af6aa8b02bffe1f9e70d6922b726 *electron-api.json -a916f0cc438258f42f43955157565e7eca14966266f3fb123c8c736bece97daa *electron-v28.2.8-darwin-arm64-dsym-snapshot.zip -3c31d0a105b0632f15aa8adc68f06dc8ca47b1fdf1e62d1436ac43af117a22fb *electron-v28.2.8-darwin-arm64-dsym.zip -dab03f1cd7b499552d503bcca2fc1c3f40a1d2c463655ca3ace20778f08e9b04 *electron-v28.2.8-darwin-arm64-symbols.zip -2965d8c8d64fb6c51f5a283a246de653bfae22fe4bf9adf6c04592afabf62f04 *electron-v28.2.8-darwin-arm64.zip -03511a34d94d27eb576ab20e3a432c082a32a298475c7a85a329e029dddc55e4 *electron-v28.2.8-darwin-x64-dsym-snapshot.zip -96089786bd2723786673561c9b6f9a154928de663f2411f10153e6c985703eef *electron-v28.2.8-darwin-x64-dsym.zip -872789c3c218ab8f98be83c7781e3e6ef0114bd39780d65eaae77e99dbbda1de *electron-v28.2.8-darwin-x64-symbols.zip -a7889addd37254f842798bdd3ca34752b75acf6d8dd456cdeb2d75590c0a9ceb *electron-v28.2.8-darwin-x64.zip -fb90b8c903407ae575f9c8f727376519c0b35ed6f01dec55b177285b5db864e3 *electron-v28.2.8-linux-arm64-debug.zip -591248f7c94a6d7c4a4d8b2fcf63c8e4347018a65e1f68ed90e5549a587062c8 *electron-v28.2.8-linux-arm64-symbols.zip -6183db1029cebd9e0fb0e4f2d24a80b0274c5265756e66cb9fa0a480b92c98ea *electron-v28.2.8-linux-arm64.zip -fb90b8c903407ae575f9c8f727376519c0b35ed6f01dec55b177285b5db864e3 *electron-v28.2.8-linux-armv7l-debug.zip -87c4c534cd1d447b9d4632585a0d79c9d31114bd39ca63df1f2384afae3aa6b7 *electron-v28.2.8-linux-armv7l-symbols.zip -2a772b65815a0d47a756eed52f76cd9f27a8c277d7998bfcfe93b84a346eb255 *electron-v28.2.8-linux-armv7l.zip -773aa1f0bbe2b79765bf498958565f63957f8ec2e42327978a143dcbbc7f1bea *electron-v28.2.8-linux-x64-debug.zip -f8cbc6f2b719cc2f623afcfde8cb1d42614708793621a7a97b328015366b9b8f *electron-v28.2.8-linux-x64-symbols.zip -e7d17ee311299dfef3d2916987a513c4c1b66ad2e417c15fa5d29699602bd6cb *electron-v28.2.8-linux-x64.zip -5f0179fd7bf3927381bde24c9fb372fe95328be0500918cd6ee7f9503fae1ef5 *electron-v28.2.8-mas-arm64-dsym-snapshot.zip -e9810019f1d7b1b5a93fd1aee8adda5a872ebfb170de6d55cdd55162b923432d *electron-v28.2.8-mas-arm64-dsym.zip -4781376244c7df89d119575e2788ad43fae4387d850ef672665688081b30997c *electron-v28.2.8-mas-arm64-symbols.zip -a3932199781970e0b2fdb805d6556287ca877b35ac19384da00474140e14c41f *electron-v28.2.8-mas-arm64.zip -326cde32079496e0d976c5b65e85e5ce208eea3d8d23cd92c9e25f0fa6b30f40 *electron-v28.2.8-mas-x64-dsym-snapshot.zip -59a2b3d28dba45ee3016f8ab49a71b0c55f99ef046476183bc36890c9d335a71 *electron-v28.2.8-mas-x64-dsym.zip -313ff88f568c39079a1b7a1011f77fa03890cb9bb53649a489643311303cc3b8 *electron-v28.2.8-mas-x64-symbols.zip -41ab9f3addea5066d7e0ace28ebaead7128a2073931473c847aa9133b7df9248 *electron-v28.2.8-mas-x64.zip -179de6dd4835216bcd3e8bb9eb4d4b54013df865f52dbf0d5214726fc31cba9a *electron-v28.2.8-win32-arm64-pdb.zip -8628dec571206001420c1d8655904883d5de7e772d51ab2101b002c22e0dd25c *electron-v28.2.8-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-arm64-toolchain-profile.zip -bb2a2a466d14c32c06ff09c42b3d1413f19fdc8a49a445d07d289fa453c268d3 *electron-v28.2.8-win32-arm64.zip -1d1efc3a1d17072bc76a4a63c8236a896d46f6f3badacd50bc5824149196d56f *electron-v28.2.8-win32-ia32-pdb.zip -9ddb1520de421a7c636160d01432c9bf111e6ef4b9a3be41b185c702c72353ac *electron-v28.2.8-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-ia32-toolchain-profile.zip -38e22f9b0a32e0fc26e81905214e244c0a5d5c19e13c8ca2329ac75b62881472 *electron-v28.2.8-win32-ia32.zip -8168296e0454377e0113a7d0f87535d3d0e0c1a8538e8079ee1aae9c7223bb02 *electron-v28.2.8-win32-x64-pdb.zip -a276e9e748fa7db970e7dcce6f4ae571d8615a44e5208c0fa3c03de08774a4aa *electron-v28.2.8-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.8-win32-x64-toolchain-profile.zip -079cc98f7933992ac7154e21e160d4a4c6b3541c26b56fc6f8438e9eabc369b9 *electron-v28.2.8-win32-x64.zip -f838e4a7c24518c5fa25d4a23acf869737cfa88761019cea4f83ebfb302363ec *electron.d.ts -4450bcc66cece4ff2373563e0123799f95645fa155577a8f380211b29e8b4ec9 *ffmpeg-v28.2.8-darwin-arm64.zip -152e3ed53098d24f356d7ec640d19efc57f7f34c39d8b8278f2586985d4a99a1 *ffmpeg-v28.2.8-darwin-x64.zip -8e108e533811febcc51f377ac8604d506663453e41c02dc818517e1ea9a4e8d5 *ffmpeg-v28.2.8-linux-arm64.zip -51ecd03435f56a2ced31b1c9dbf281955ba82a814ca0214a4292bdc711e5a45c *ffmpeg-v28.2.8-linux-armv7l.zip -acc9dc3765f68b7563045e2d0df11bbef6b41be0a1c34bbf9fa778f36eefb42f *ffmpeg-v28.2.8-linux-x64.zip -15a2a4a28a66e65122eb4f2bd796ccd5b6ed45420a034878affd002fc8c290dc *ffmpeg-v28.2.8-mas-arm64.zip -2dfe2f524c5220f50c7b6fe08605a67631b5520e0c82842e1f41f677cac17643 *ffmpeg-v28.2.8-mas-x64.zip -313e2979f0df88715159c0737bfbb5ae1d5c79fb9820e94d2a93ba71d3324ecd *ffmpeg-v28.2.8-win32-arm64.zip -9e73bc07563aefa8b9625676939a410b35a823d961b96da0e8edd90d7e5fb47b *ffmpeg-v28.2.8-win32-ia32.zip -1b11042defc8a3f403e5567fa4a4b8c59b224f3b7b52d44d6c7197b96af7b53b *ffmpeg-v28.2.8-win32-x64.zip -1e2e9480d4228f6bbc731ff7ee413b9e97656c36b15418d20681a76d82902b86 *hunspell_dictionaries.zip -8c8b967cf4c78ed9bbf4921b2c616257f45b137412eb3bc64176066c3e47bbe8 *libcxx-objects-v28.2.8-linux-arm64.zip -56af259535ccfaac295b82ce68686f9582265cb2ebe2783852f518c0fabc8a1e *libcxx-objects-v28.2.8-linux-armv7l.zip -b590e001dc98e32e5952ca69573e6f1bcec5e2f2d99052d1089ab72084cccea1 *libcxx-objects-v28.2.8-linux-x64.zip -c0634d5c92f0a2983b17c866f7d3694cb75f6e78cd07b10d9488ef46acc66a50 *libcxx_headers.zip -99ee16441d9eb2b92a05d5a5c9b9dc4cdfab33cb09595e9d78fd2ba503dead5b *libcxxabi_headers.zip -a95de1da301d641caaafaea9869c4c7834c254f818ac0c10d97402b2220c8be3 *mksnapshot-v28.2.8-darwin-arm64.zip -e5ef6b35d7cd807f93babfedbbde513ab6053ad9fb80b0f7abc1bfda414daaa1 *mksnapshot-v28.2.8-darwin-x64.zip -eeb6c5b7962af8d5cfaa97b2cf96d312d0ad57a3abb3e00774d50ea2e005bb9b *mksnapshot-v28.2.8-linux-arm64-x64.zip -0adacd0767469f90400b1f17ba8ac3ccb33cfeb11a8ef54d70bc8adb7cc306dc *mksnapshot-v28.2.8-linux-armv7l-x64.zip -5242817f1f26e10804e7e2446d0a8a64e8b2958cdba01e79d89db883d9d960d0 *mksnapshot-v28.2.8-linux-x64.zip -0ecb67673508c10f4fe08e7cb80300b9a8f507f50994c79caf302ff78ef748ca *mksnapshot-v28.2.8-mas-arm64.zip -19429da56077f12de4d4563f49c55f4f1f0fe61f66863804640fc55e65ee98f9 *mksnapshot-v28.2.8-mas-x64.zip -c7b47ae63c2f6eb07b06379206e6f215fbcb2b9a49faa72ca850bf8f9b998c4c *mksnapshot-v28.2.8-win32-arm64-x64.zip -0032660a9f8575a153951f29adae49a18e400b40906eec803fe7e3d2e970503d *mksnapshot-v28.2.8-win32-ia32.zip -2c71c9a2bd4441e580dc3083073e712fba94e0236415c8ab35320da52f492508 *mksnapshot-v28.2.8-win32-x64.zip +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 diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index 13aa4c7e87b..63d24a93ab6 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,6 +1,7 @@ -9f982cc91b28778dd8638e4f94563b0c2a1da7aba62beb72bd427721035ab553 node-v18.18.2-darwin-arm64.tar.gz -5bb8da908ed590e256a69bf2862238c8a67bc4600119f2f7721ca18a7c810c0f node-v18.18.2-darwin-x64.tar.gz -0c9a6502b66310cb26e12615b57304e91d92ac03d4adcb91c1906351d7928f0d node-v18.18.2-linux-arm64.tar.gz -7a3b34a6fdb9514bc2374114ec6df3c36113dc5075c38b22763aa8f106783737 node-v18.18.2-linux-armv7l.tar.gz -a44c3e7f8bf91e852c928e5d8bd67ca316b35e27eec1d8acbe3b9dbe03688dab node-v18.18.2-linux-x64.tar.gz -54884183ff5108874c091746465e8156ae0acc68af589cc10bc41b3927db0f4a win-x64/node.exe +31d2d46ae8d8a3982f54e2ff1e60c2e4a8e80bf78a3e8b46dcaac95ac5d7ce6a node-v20.9.0-darwin-arm64.tar.gz +fc5b73f2a78c17bbe926cdb1447d652f9f094c79582f1be6471b4b38a2e1ccc8 node-v20.9.0-darwin-x64.tar.gz +d2a7dbeeb274bfd16b579d2cafb92f673010df36c83a5b55de3916aad6806a6a node-v20.9.0-linux-arm64.tar.gz +a28a0de05177106d241ef426b3e006022bc7d242224adace7565868bd9ee6c06 node-v20.9.0-linux-armv7l.tar.gz +f0919f092fbf74544438907fa083c21e76b2d7a4bc287f0607ada1553ef16f60 node-v20.9.0-linux-x64.tar.gz +54e165b89e75158993910053db5b0e652c1826521e624126de5ca6de9ff7b06d win-arm64/node.exe +538140015da83597ea7e7ef5e108ebac8a2dc4784b2a4134222b6c27c39f90ad win-x64/node.exe diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 4979682935e..dce2b85d658 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -62,7 +62,13 @@ const CORE_TYPES = [ 'EventTarget', 'BroadcastChannel', 'performance', - 'Blob' + 'Blob', + 'crypto', + 'File', + 'fetch', + 'RequestInit', + 'Headers', + 'Response' ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser @@ -164,6 +170,62 @@ const RULES = [ '@types/node' // no node.js ] }, + // Common: vs/workbench/api/common/extHostTypes.ts + { + target: '**/vs/workbench/api/common/extHostTypes.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/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 + ] + }, // Common { target: '**/vs/**/common/**', diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 864d61f1452..039f222135d 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -63,7 +63,13 @@ const CORE_TYPES = [ 'EventTarget', 'BroadcastChannel', 'performance', - 'Blob' + 'Blob', + 'crypto', + 'File', + 'fetch', + 'RequestInit', + 'Headers', + 'Response' ]; // Types that are defined in a common layer but are known to be only @@ -179,6 +185,70 @@ const RULES: IRule[] = [ ] }, + // Common: vs/workbench/api/common/extHostTypes.ts + { + target: '**/vs/workbench/api/common/extHostTypes.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/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 + ] + }, + // Common { target: '**/vs/**/common/**', diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js index efcb3220084..1bfe7f573f6 100644 --- a/build/lib/preLaunch.js +++ b/build/lib/preLaunch.js @@ -12,7 +12,7 @@ const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; const rootDir = path.resolve(__dirname, '..', '..'); function runProcess(command, args = []) { return new Promise((resolve, reject) => { - const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env, shell: process.platform === 'win32' }); child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); child.on('error', reject); }); diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts index 3d3f513b591..d6776e62798 100644 --- a/build/lib/preLaunch.ts +++ b/build/lib/preLaunch.ts @@ -14,7 +14,7 @@ const rootDir = path.resolve(__dirname, '..', '..'); function runProcess(command: string, args: ReadonlyArray = []) { return new Promise((resolve, reject) => { - const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); + const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env, shell: process.platform === 'win32' }); child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); child.on('error', reject); }); diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 497957a39c5..e6291132d56 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -647,6 +647,9 @@ "--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", diff --git a/build/lib/util.js b/build/lib/util.js index ed52776c2c0..02ce049b00b 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -34,7 +34,6 @@ const rename = require("gulp-rename"); const path = require("path"); const fs = require("fs"); const _rimraf = require("rimraf"); -const VinylFile = require("vinyl"); const url_1 = require("url"); const ternaryStream = require("ternary-stream"); const root = path.dirname(path.dirname(__dirname)); diff --git a/build/lib/watch/watch-win32.js b/build/lib/watch/watch-win32.js index 49094e915e4..934d8e8110f 100644 --- a/build/lib/watch/watch-win32.js +++ b/build/lib/watch/watch-win32.js @@ -70,8 +70,8 @@ module.exports = function (pattern, options) { return f; }); return watcher - .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git - .pipe(filter(pattern)) + .pipe(filter(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git + .pipe(filter(pattern, { dot: options.dot })) .pipe(es.map(function (file, cb) { fs.stat(file.path, function (err, stat) { if (err && err.code === 'ENOENT') { diff --git a/build/lib/watch/watch-win32.ts b/build/lib/watch/watch-win32.ts index fa65a5bdeb2..afde6a79f22 100644 --- a/build/lib/watch/watch-win32.ts +++ b/build/lib/watch/watch-win32.ts @@ -70,7 +70,7 @@ function watch(root: string): Stream { const cache: { [cwd: string]: Stream } = Object.create(null); -module.exports = function (pattern: string | string[] | filter.FileFunction, options?: { cwd?: string; base?: string }) { +module.exports = function (pattern: string | string[] | filter.FileFunction, options?: { cwd?: string; base?: string; dot?: boolean }) { options = options || {}; const cwd = path.normalize(options.cwd || process.cwd()); @@ -86,8 +86,8 @@ module.exports = function (pattern: string | string[] | filter.FileFunction, opt }); return watcher - .pipe(filter(['**', '!.git{,/**}'])) // ignore all things git - .pipe(filter(pattern)) + .pipe(filter(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git + .pipe(filter(pattern, { dot: options.dot })) .pipe(es.map(function (file: File, cb) { fs.stat(file.path, function (err, stat) { if (err && err.code === 'ENOENT') { return cb(undefined, file); } diff --git a/build/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js index bdb265b6fec..d843c090063 100644 --- a/build/linux/debian/dep-lists.js +++ b/build/linux/debian/dep-lists.js @@ -57,8 +57,7 @@ exports.referenceGeneratedDepsByArch = { 'libxkbcommon0 (>= 0.5.0)', 'libxkbfile1 (>= 1:1.1.0)', 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' + 'xdg-utils (>= 1.0.2)' ], 'armhf': [ 'ca-certificates', diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 3d6c2eba6e9..4028370cd02 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -57,8 +57,7 @@ export const referenceGeneratedDepsByArch = { 'libxkbcommon0 (>= 0.5.0)', 'libxkbfile1 (>= 1:1.1.0)', 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' + 'xdg-utils (>= 1.0.2)' ], 'armhf': [ 'ca-certificates', diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index 80c247d1129..bff0c9a25df 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/120.0.6099.268:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156: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 9f1a068b8d7..226310e1258 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/120.0.6099.268:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/122.0.6261.156: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 bd84fc146dc..8be477290bb 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -38,10 +38,12 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.10)(64bit)', 'libc.so.6(GLIBC_2.11)(64bit)', + 'libc.so.6(GLIBC_2.12)(64bit)', 'libc.so.6(GLIBC_2.14)(64bit)', 'libc.so.6(GLIBC_2.15)(64bit)', 'libc.so.6(GLIBC_2.16)(64bit)', '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.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', @@ -109,8 +111,6 @@ exports.referenceGeneratedDepsByArch = { 'libxkbcommon.so.0()(64bit)', 'libxkbcommon.so.0(V_0.5.0)(64bit)', 'libxkbfile.so.1()(64bit)', - 'libz.so.1()(64bit)', - 'libz.so.1(ZLIB_1.2.3.4)(64bit)', 'rpmlib(FileDigests) <= 4.6.0-1', 'rtld(GNU_HASH)', 'xdg-utils' @@ -134,9 +134,12 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6', 'libc.so.6(GLIBC_2.10)', 'libc.so.6(GLIBC_2.11)', + 'libc.so.6(GLIBC_2.12)', + 'libc.so.6(GLIBC_2.14)', 'libc.so.6(GLIBC_2.15)', 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', + 'libc.so.6(GLIBC_2.18)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', 'libc.so.6(GLIBC_2.6)', @@ -237,6 +240,7 @@ exports.referenceGeneratedDepsByArch = { 'libatspi.so.0()(64bit)', 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.18)(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 82a4fe7698d..24b18d504c8 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -37,10 +37,12 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.10)(64bit)', 'libc.so.6(GLIBC_2.11)(64bit)', + 'libc.so.6(GLIBC_2.12)(64bit)', 'libc.so.6(GLIBC_2.14)(64bit)', 'libc.so.6(GLIBC_2.15)(64bit)', 'libc.so.6(GLIBC_2.16)(64bit)', '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.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', @@ -108,8 +110,6 @@ export const referenceGeneratedDepsByArch = { 'libxkbcommon.so.0()(64bit)', 'libxkbcommon.so.0(V_0.5.0)(64bit)', 'libxkbfile.so.1()(64bit)', - 'libz.so.1()(64bit)', - 'libz.so.1(ZLIB_1.2.3.4)(64bit)', 'rpmlib(FileDigests) <= 4.6.0-1', 'rtld(GNU_HASH)', 'xdg-utils' @@ -133,9 +133,12 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6', 'libc.so.6(GLIBC_2.10)', 'libc.so.6(GLIBC_2.11)', + 'libc.so.6(GLIBC_2.12)', + 'libc.so.6(GLIBC_2.14)', 'libc.so.6(GLIBC_2.15)', 'libc.so.6(GLIBC_2.16)', 'libc.so.6(GLIBC_2.17)', + 'libc.so.6(GLIBC_2.18)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', 'libc.so.6(GLIBC_2.6)', @@ -236,6 +239,7 @@ export const referenceGeneratedDepsByArch = { 'libatspi.so.0()(64bit)', 'libc.so.6()(64bit)', 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 72dd74f8986..bcac781e265 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -36,6 +36,7 @@ function yarnInstall(dir, opts) { ...(opts ?? {}), cwd: dir, stdio: 'inherit', + shell: true }; const raw = process.env['npm_config_argv'] || '{}'; diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index edf0d98c3d5..fdb01f579d6 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -10,13 +10,10 @@ const minorNodeVersion = parseInt(nodeVersion[2]); const patchNodeVersion = parseInt(nodeVersion[3]); if (!process.env['VSCODE_SKIP_NODE_VERSION_CHECK']) { - if (majorNodeVersion < 18 || (majorNodeVersion === 18 && minorNodeVersion < 15)) { - console.error('\x1b[1;31m*** Please use node.js versions >=18.15.x and <19.\x1b[0;0m'); + if (majorNodeVersion < 20) { + console.error('\x1b[1;31m*** Please use latest Node.js v20 LTS for development.\x1b[0;0m'); err = true; } - if (majorNodeVersion >= 19) { - console.warn('\x1b[1;31m*** Warning: Versions of node.js >= 19 have not been tested.\x1b[0;0m') - } } const path = require('path'); @@ -102,7 +99,8 @@ function installHeaders() { const yarnResult = cp.spawnSync(yarn, ['install'], { env: process.env, cwd: path.join(__dirname, 'gyp'), - stdio: 'inherit' + stdio: 'inherit', + shell: true }); if (yarnResult.error || yarnResult.status !== 0) { console.error(`Installing node-gyp failed`); @@ -114,7 +112,7 @@ function installHeaders() { // file checked into our repository. So from that point it is save to construct the path // to that executable const node_gyp = path.join(__dirname, 'gyp', 'node_modules', '.bin', 'node-gyp.cmd'); - const result = cp.execFileSync(node_gyp, ['list'], { encoding: 'utf8' }); + const result = cp.execFileSync(node_gyp, ['list'], { encoding: 'utf8', shell: true }); const versions = new Set(result.split(/\n/g).filter(line => !line.startsWith('gyp info')).map(value => value)); const local = getHeaderInfo(path.join(__dirname, '..', '..', '.yarnrc')); @@ -122,7 +120,7 @@ function installHeaders() { if (local !== undefined && !versions.has(local.target)) { // Both disturl and target come from a file checked into our repository - cp.execFileSync(node_gyp, ['install', '--dist-url', local.disturl, local.target]); + cp.execFileSync(node_gyp, ['install', '--dist-url', local.disturl, local.target], { shell: true }); } // Avoid downloading headers for Windows arm64 till we move to Nodejs v19 in remote @@ -139,7 +137,7 @@ function installHeaders() { process.env['npm_config_arch'] !== "arm64" && process.arch !== "arm64") { // Both disturl and target come from a file checked into our repository - cp.execFileSync(node_gyp, ['install', '--dist-url', remote.disturl, remote.target]); + cp.execFileSync(node_gyp, ['install', '--dist-url', remote.disturl, remote.target], { shell: true }); } } diff --git a/build/package.json b/build/package.json index c9cd0af9f32..2b89bbc1c99 100644 --- a/build/package.json +++ b/build/package.json @@ -14,7 +14,7 @@ "@types/fancy-log": "^1.3.0", "@types/fs-extra": "^9.0.12", "@types/glob": "^7.1.1", - "@types/gulp": "^4.0.5", + "@types/gulp": "^4.0.17", "@types/gulp-concat": "^0.0.32", "@types/gulp-filter": "^3.0.32", "@types/gulp-gzip": "^0.0.31", @@ -26,7 +26,7 @@ "@types/minimist": "^1.2.1", "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/pump": "^1.0.1", "@types/rimraf": "^2.0.4", "@types/through": "^0.0.29", diff --git a/build/tsconfig.build.json b/build/tsconfig.build.json index 801c7735b06..4534420208f 100644 --- a/build/tsconfig.build.json +++ b/build/tsconfig.build.json @@ -3,7 +3,8 @@ "compilerOptions": { "allowJs": false, "checkJs": false, - "noEmit": false + "noEmit": false, + "skipLibCheck": true }, "include": [ "**/*.ts" diff --git a/build/yarn.lock b/build/yarn.lock index 03248b45c22..3131c43217c 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -404,14 +404,6 @@ "@types/node" "*" "@types/responselike" "^1.0.0" -"@types/chokidar@*": - version "1.7.5" - resolved "https://registry.yarnpkg.com/@types/chokidar/-/chokidar-1.7.5.tgz#1fa78c8803e035bed6d98e6949e514b133b0c9b6" - integrity sha512-PDkSRY7KltW3M60hSBlerxI8SFPXsO3AL/aRVsO4Kh9IHRW74Ih75gUuTd/aE4LSSFqypb10UIX3QzOJwBQMGQ== - dependencies: - "@types/events" "*" - "@types/node" "*" - "@types/debounce@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.0.0.tgz#417560200331e1bb84d72da85391102c2fcd61b7" @@ -429,6 +421,11 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA== +"@types/expect@^1.20.4": + version "1.20.4" + resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" + integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== + "@types/fancy-log@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@types/fancy-log/-/fancy-log-1.3.0.tgz#a61ab476e5e628cd07a846330df53b85e05c8ce0" @@ -503,14 +500,15 @@ dependencies: "@types/node" "*" -"@types/gulp@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.5.tgz#f5f498d5bf9538364792de22490a12c0e6bc5eb4" - integrity sha512-nx1QjPTiRpvLfYsZ7MBu7bT6Cm7tAXyLbY0xbdx2IEMxCK2v2urIhJMQZHW0iV1TskM71Xl6p2uRRuWDbk+/7g== +"@types/gulp@^4.0.17": + version "4.0.17" + resolved "https://registry.yarnpkg.com/@types/gulp/-/gulp-4.0.17.tgz#b314c3762d08d8d69b7c0b86f78d069bafd65009" + integrity sha512-+pKQynu2C/HS16kgmDlAicjtFYP8kaa86eE9P0Ae7GB5W29we/E2TIdbOWtEZD5XkpY+jr8fyqfwO6SWZecLpQ== dependencies: - "@types/chokidar" "*" - "@types/undertaker" "*" + "@types/node" "*" + "@types/undertaker" ">=1.2.6" "@types/vinyl-fs" "*" + chokidar "^3.3.1" "@types/http-cache-semantics@*": version "4.0.4" @@ -574,10 +572,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@18.x": - version "18.18.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.9.tgz#5527ea1832db3bba8eb8023ce8497b7d3f299592" - integrity sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== dependencies: undici-types "~5.26.4" @@ -634,13 +632,14 @@ resolved "https://registry.yarnpkg.com/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz#4306d4a03d7acedb974b66530832b90729e1d1da" integrity sha512-Z4TYuEKn9+RbNVk1Ll2SS4x1JeLHecolIbM/a8gveaHsW0Hr+RQMraZACwTO2VD7JvepgA6UO1A1VrbktQrIbQ== -"@types/undertaker@*": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/undertaker/-/undertaker-1.2.0.tgz#d39a81074b4f274eb656870fc904a70737e00f8e" - integrity sha512-bx/5nZCGkasXs6qaA3B6SVDjBZqdyk04UO12e0uEPSzjt5H8jEJw0DKe7O7IM0hM2bVHRh70pmOH7PEHqXwzOw== +"@types/undertaker@>=1.2.6": + version "1.2.11" + resolved "https://registry.yarnpkg.com/@types/undertaker/-/undertaker-1.2.11.tgz#d9e08b72c4bea5fc40e5bad63ad5a1a2b675e3ca" + integrity sha512-j1Z0V2ByRHr8ZK7eOeGq0LGkkdthNFW0uAZGY22iRkNQNL9/vAV0yFPr1QN3FM/peY5bxs9P+1f0PYJTQVa5iA== dependencies: - "@types/events" "*" + "@types/node" "*" "@types/undertaker-registry" "*" + async-done "~1.3.2" "@types/vinyl-fs@*": version "2.4.9" @@ -653,10 +652,11 @@ "@types/vinyl" "*" "@types/vinyl@*": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.2.tgz#4f3b8dae8f5828d3800ef709b0cff488ee852de3" - integrity sha512-2iYpNuOl98SrLPBZfEN9Mh2JCJ2EI9HU35SfgBEb51DcmaHkhp8cKMblYeBqMQiwXMgAD3W60DbQ4i/UdLiXhw== + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.12.tgz#17642ca9a8ae10f3db018e9f885da4188db4c6e6" + integrity sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw== dependencies: + "@types/expect" "^1.20.4" "@types/node" "*" "@types/workerpool@^6.4.0": @@ -763,6 +763,14 @@ anymatch@^3.1.1, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -795,6 +803,16 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +async-done@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" + integrity sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.2" + process-nextick-args "^2.0.0" + stream-exhaust "^1.0.1" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -996,6 +1014,21 @@ chokidar@3.5.1: optionalDependencies: fsevents "~2.3.1" +chokidar@^3.3.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -1460,6 +1493,11 @@ fsevents@~2.3.1: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -1486,7 +1524,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= -glob-parent@^5.1.1, glob-parent@~5.1.0: +glob-parent@^5.1.1, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -2025,11 +2063,6 @@ mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.17.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" - integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== - nan@^2.18.0: version "2.19.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.19.0.tgz#bb58122ad55a6c5bc973303908d5b16cfdd5a8c0" @@ -2096,7 +2129,7 @@ object-keys@^1.0.12: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -2192,7 +2225,7 @@ plugin-error@1.0.1, plugin-error@^1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" -prebuild-install@^7.0.1, prebuild-install@^7.1.1: +prebuild-install@^7.0.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== @@ -2210,6 +2243,24 @@ prebuild-install@^7.0.1, prebuild-install@^7.1.1: tar-fs "^2.0.0" tunnel-agent "^0.6.0" +prebuild-install@^7.1.1: + 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" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + priorityqueuejs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz#2ee4f23c2560913e08c07ce5ccdd6de3df2c5af8" @@ -2296,6 +2347,13 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" @@ -2436,6 +2494,11 @@ stoppable@^1.1.0: resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== +stream-exhaust@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d" + integrity sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw== + stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" @@ -2586,15 +2649,7 @@ tree-sitter-typescript@^0.20.5: nan "^2.18.0" tree-sitter "^0.20.6" -tree-sitter@^0.20.5: - version "0.20.5" - resolved "https://registry.yarnpkg.com/tree-sitter/-/tree-sitter-0.20.5.tgz#554741ee06b984824dd5082353aa2a28bcefa271" - integrity sha512-xjxkKCKV7F2F5HWmyRE4bosoxkbxe9lYvFRc/nzmtHNqFNTwYwh0oWVVEt0VnbupZHMirEQW7vDx8ddJn72tjg== - dependencies: - nan "^2.17.0" - prebuild-install "^7.1.1" - -tree-sitter@^0.20.6: +tree-sitter@^0.20.5, tree-sitter@^0.20.6: version "0.20.6" resolved "https://registry.yarnpkg.com/tree-sitter/-/tree-sitter-0.20.6.tgz#fec52e5d7cc6c583135756479f2440dd89b25cbe" integrity sha512-GxJodajVpfgb3UREzzIbtA1hyRnTxVbWVXrbC6sk4xTMH5ERMBJk9HJNq4c8jOJeUaIOmLcwg+t6mez/PDvGqg== diff --git a/cgmanifest.json b/cgmanifest.json index 891a0b0cb32..f1e4192dc28 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "14d11e5bb9b5b1cd51f7b19546e74a73cab42084" + "commitHash": "f1a45d7ded05d64ca8136cc142ddc0c271b1dd43" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "120.0.6099.291" + "version": "122.0.6261.156" }, { "component": { @@ -48,7 +48,7 @@ "git": { "name": "ffmpeg", "repositoryUrl": "https://chromium.googlesource.com/chromium/third_party/ffmpeg", - "commitHash": "e1ca3f06adec15150a171bc38f550058b4bbb23b" + "commitHash": "17525de887d54b970ffdd421a0879c1db1952307" } }, "isOnlyProductionDependency": true, @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "8a01b3dcb7d08a48bfd3e6bf85ef49faa1454839" + "commitHash": "22f383dcd529d6bf790856db614a35fea78e825f" } }, "isOnlyProductionDependency": true, - "version": "18.18.2" + "version": "20.9.0" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "31cd9d1f61714e20f1067d726404600ab7281698" + "commitHash": "f9ed0eaee4b172733872c2f84e5061882dd08e5c" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "28.2.8" + "version": "29.4.0" }, { "component": { diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 4d62806a93c..3be3815a748 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -10,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -34,51 +43,51 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -87,113 +96,176 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ - "event-listener", + "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" -version = "1.8.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ - "concurrent-queue 2.2.0", - "event-listener", + "concurrent-queue", + "event-listener-strategy 0.5.2", "futures-core", + "pin-project-lite", ] [[package]] name = "async-io" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ + "async-lock 2.8.0", "autocfg", - "concurrent-queue 1.2.4", - "futures-lite", - "libc", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", "log", - "once_cell", "parking", - "polling", + "polling 2.8.0", + "rustix 0.37.27", "slab", - "socket2", + "socket2 0.4.10", "waker-fn", - "winapi", +] + +[[package]] +name = "async-io" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.0", + "rustix 0.38.34", + "slab", + "tracing", + "windows-sys 0.52.0", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ - "event-listener", + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", ] [[package]] name = "async-process" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ - "async-io", - "async-lock", - "autocfg", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", "blocking", "cfg-if", - "event-listener", - "futures-lite", - "rustix", - "signal-hook", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.34", "windows-sys 0.48.0", ] [[package]] name = "async-recursion" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", +] + +[[package]] +name = "async-signal" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" +dependencies = [ + "async-io 2.3.2", + "async-lock 3.3.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.34", + "signal-hook-registry", + "slab", + "windows-sys 0.52.0", ] [[package]] name = "async-task" -version = "4.4.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] [[package]] name = "base64" -version = "0.21.2" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bit-vec" @@ -209,72 +281,65 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "block-padding" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array", ] [[package]] name = "blocking" -version = "1.3.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ "async-channel", - "async-lock", + "async-lock 3.3.0", "async-task", - "atomic-waker", - "fastrand", - "futures-lite", - "log", + "futures-io", + "futures-lite 2.3.0", + "piper", ] [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -284,58 +349,56 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "winapi", + "windows-targets 0.52.5", ] [[package]] name = "clap" -version = "4.3.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.0" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", - "bitflags 1.3.2", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "code-cli" @@ -389,67 +452,48 @@ dependencies = [ "zip", ] -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "concurrent-queue" -version = "1.2.4" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" -dependencies = [ - "cache-padded", -] - -[[package]] -name = "concurrent-queue" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] name = "const_format" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" dependencies = [ "proc-macro2", "quote", @@ -458,9 +502,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -468,42 +512,42 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "58ebf8d6963185c7625d2c3c3962d99eb8936637b1427536d21dc36ae402ebad" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" @@ -515,55 +559,20 @@ dependencies = [ "typenum", ] -[[package]] -name = "cxx" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88abab2f5abbe4c56e8f1fb431b784d710b709888f35755a160e62e33fe38e8" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0c11acd0e63bae27dcd2afced407063312771212b7a823b4fd72d633be30fb" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.18", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3816ed957c008ccd4728485511e3d9aaf7db419aa321e3d2c5a2f3411e36c8" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26acccf6f445af85ea056362561a24ef56cdc15fcc685f03aec50b9c702cb6d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.18", -] - [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] [[package]] name = "derivative" @@ -573,7 +582,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", ] [[package]] @@ -590,9 +599,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -648,18 +657,18 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] [[package]] name = "enumflags2" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d" dependencies = [ "enumflags2_derive", "serde", @@ -667,13 +676,13 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] @@ -684,23 +693,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -710,31 +708,90 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] -name = "fastrand" -version = "1.8.0" +name = "event-listener" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] -name = "filetime" -version = "0.2.17" +name = "fastrand" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", - "windows-sys 0.36.1", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", ] [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "libz-sys", @@ -764,18 +821,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -788,9 +845,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -798,15 +855,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -815,17 +872,17 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -835,33 +892,43 @@ dependencies = [ ] [[package]] -name = "futures-macro" -version = "0.3.28" +name = "futures-lite" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -877,9 +944,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -892,7 +959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -908,15 +975,21 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "h2" version = "0.3.26" @@ -929,7 +1002,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -944,30 +1017,21 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -992,9 +1056,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1003,9 +1067,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1020,15 +1084,15 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1041,7 +1105,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -1063,33 +1127,32 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1107,19 +1170,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] name = "indicatif" -version = "0.17.4" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db45317f37ef454e6519b6c3ed7d377e5f23346f0823f86e65ca36912d1d0ef8" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", "instant", @@ -1140,9 +1203,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -1153,16 +1216,16 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "ipnet" -version = "2.5.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-docker" @@ -1173,18 +1236,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is-terminal" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.48.0", -] - [[package]] name = "is-wsl" version = "0.4.0" @@ -1196,32 +1247,38 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.4" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "keyring" -version = "2.0.3" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e319fe0cb5b29a55cdb228df3f651b6c8cdc5b19520f3e62c8f111dc2582026c" +checksum = "363387f0019d714aa60cc30ab4fe501a747f4c08fc58f069dd14be971bd495a0" dependencies = [ "byteorder", "lazy_static", "linux-keyutils", "secret-service", "security-framework", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1232,37 +1289,38 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] [[package]] name = "libz-sys" -version = "1.1.12" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" dependencies = [ "cc", "pkg-config", "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" -dependencies = [ - "cc", -] - [[package]] name = "linux-keyutils" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f27bb67f6dd1d0bb5ab582868e4f65052e58da6401188a08f0da09cf512b84b" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "libc", ] @@ -1273,10 +1331,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] -name = "lock_api" -version = "0.4.9" +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1284,9 +1348,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.18" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "md5" @@ -1296,9 +1360,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memoffset" @@ -1310,16 +1374,25 @@ dependencies = [ ] [[package]] -name = "mime" -version = "0.3.16" +name = "memoffset" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -1355,31 +1428,30 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", - "static_assertions", + "memoffset 0.7.1", ] [[package]] name = "ntapi" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] [[package]] name = "num" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", @@ -1391,11 +1463,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", "rand 0.8.5", @@ -1403,28 +1474,33 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.2" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -1433,11 +1509,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -1445,20 +1520,20 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi", "libc", ] @@ -1469,28 +1544,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] -name = "once_cell" -version = "1.17.2" +name = "object" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16814a067484415fda653868c9be0ac5f2abd2ef5d951082a5f2fe1b3662944" +checksum = "3a083c0c7e5e4a8ec4176346cf61f67ac674e8bfb059d9226e1c54a96b377c12" dependencies = [ "is-wsl", + "libc", "pathdiff", ] [[package]] name = "openssl" -version = "0.10.60" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -1501,13 +1586,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", ] [[package]] @@ -1518,9 +1603,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.96" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -1591,25 +1676,25 @@ dependencies = [ [[package]] name = "os_info" -version = "3.7.0" +version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" dependencies = [ "log", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "parking" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -1617,22 +1702,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.5.1", "smallvec", - "windows-sys 0.36.1", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.9" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" @@ -1642,35 +1727,35 @@ checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.0" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1679,62 +1764,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkg-config" -version = "0.3.25" +name = "piper" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +dependencies = [ + "atomic-waker", + "fastrand 2.1.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "polling" -version = "2.3.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", + "bitflags 1.3.2", "cfg-if", + "concurrent-queue", "libc", "log", - "wepoll-ffi", - "winapi", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.52.0", ] [[package]] name = "portable-atomic" -version = "1.3.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "thiserror", - "toml", + "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.80" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.28" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1798,7 +1916,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.15", ] [[package]] @@ -1812,38 +1930,50 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.7", - "redox_syscall 0.2.16", + "getrandom 0.2.15", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.8.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1852,15 +1982,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64", "bytes", @@ -1880,9 +2010,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -1898,9 +2030,9 @@ dependencies = [ [[package]] name = "rmp" -version = "0.8.11" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ "byteorder", "num-traits", @@ -1909,9 +2041,9 @@ dependencies = [ [[package]] name = "rmp-serde" -version = "1.1.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" dependencies = [ "byteorder", "rmp", @@ -1921,7 +2053,7 @@ dependencies = [ [[package]] name = "russh" version = "0.37.1" -source = "git+https://github.com/microsoft/vscode-russh?branch=main#6a15199c784c0b6d171a6fec09ed730a5cd1350d" +source = "git+https://github.com/microsoft/vscode-russh?branch=main#fd4f608a83753f9f3e137f95600faffede71cf65" dependencies = [ "async-trait", "bitflags 1.3.2", @@ -1950,7 +2082,7 @@ dependencies = [ [[package]] name = "russh-cryptovec" version = "0.7.0" -source = "git+https://github.com/microsoft/vscode-russh?branch=main#6a15199c784c0b6d171a6fec09ed730a5cd1350d" +source = "git+https://github.com/microsoft/vscode-russh?branch=main#fd4f608a83753f9f3e137f95600faffede71cf65" dependencies = [ "libc", "winapi", @@ -1959,7 +2091,7 @@ dependencies = [ [[package]] name = "russh-keys" version = "0.37.1" -source = "git+https://github.com/microsoft/vscode-russh?branch=main#6a15199c784c0b6d171a6fec09ed730a5cd1350d" +source = "git+https://github.com/microsoft/vscode-russh?branch=main#fd4f608a83753f9f3e137f95600faffede71cf65" dependencies = [ "bit-vec", "byteorder", @@ -1984,46 +2116,67 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "0.37.25" +name = "rustc-demangle" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] [[package]] -name = "ryu" -version = "1.0.11" +name = "rustix" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys 0.52.0", ] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secret-service" @@ -2043,11 +2196,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -2056,9 +2209,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -2066,38 +2219,38 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.9" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -2106,13 +2259,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", ] [[package]] @@ -2129,9 +2282,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -2140,9 +2293,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2161,50 +2314,50 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" -[[package]] -name = "signal-hook" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.10.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2213,21 +2366,21 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2236,20 +2389,26 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sysinfo" -version = "0.29.0" +version = "0.29.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f1dc6930a439cc5d154221b5387d153f8183529b07c19aca24ea31e0a167e1" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" dependencies = [ "cfg-if", "core-foundation-sys", @@ -2282,9 +2441,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" dependencies = [ "filetime", "libc", @@ -2293,61 +2452,54 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", + "fastrand 2.1.0", + "rustix 0.38.34", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] name = "time" -version = "0.3.21" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ + "deranged", + "num-conv", + "powerfmt", "serde", "time-core", ] [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tinyvec" @@ -2360,17 +2512,17 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.2" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -2378,7 +2530,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.7", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -2386,13 +2538,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.65", ] [[package]] @@ -2407,9 +2559,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.11" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -2432,9 +2584,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", @@ -2442,16 +2594,23 @@ dependencies = [ "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] -name = "toml" -version = "0.5.9" +name = "toml_datetime" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "serde", + "indexmap 2.2.6", + "toml_datetime", + "winnow", ] [[package]] @@ -2462,11 +2621,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2474,29 +2632,29 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" @@ -2548,46 +2706,47 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ + "memoffset 0.9.1", "tempfile", "winapi", ] [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -2597,9 +2756,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "url" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -2626,11 +2785,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.15", "serde", ] @@ -2648,17 +2807,16 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -2676,9 +2834,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2686,24 +2844,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2713,9 +2871,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2723,28 +2881,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.65", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-streams" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -2755,23 +2913,14 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2788,15 +2937,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2804,34 +2944,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -2840,158 +2958,144 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "windows_i686_gnu" -version = "0.48.0" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.4.1" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] @@ -3017,20 +3121,22 @@ dependencies = [ [[package]] name = "xattr" -version = "0.2.3" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", ] [[package]] name = "xdg-home" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" dependencies = [ - "nix", + "libc", "winapi", ] @@ -3046,9 +3152,9 @@ dependencies = [ [[package]] name = "zbus" -version = "3.13.1" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c3d77c9966c28321f1907f0b6c5a5561189d1f7311eea6d94180c6be9daab29" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" dependencies = [ "async-broadcast", "async-process", @@ -3057,7 +3163,7 @@ dependencies = [ "byteorder", "derivative", "enumflags2", - "event-listener", + "event-listener 2.5.3", "futures-core", "futures-sink", "futures-util", @@ -3082,24 +3188,23 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.13.1" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e341d12edaff644e539ccbbf7f161601294c9a84ed3d7e015da33155b435af" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", - "syn 1.0.103", - "winnow", + "syn 1.0.109", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "2.5.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82441e6033be0a741157a72951a3e4957d519698f3a824439cc131c5ba77ac2a" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" dependencies = [ "serde", "static_assertions", @@ -3108,9 +3213,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" [[package]] name = "zip" @@ -3127,9 +3232,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.14.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622cc473f10cef1b0d73b7b34a266be30ebdcfaea40ec297dd8cbda088f9f93c" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" dependencies = [ "byteorder", "enumflags2", @@ -3141,14 +3246,14 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.14.0" +version = "3.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d9c1b57352c25b778257c661f3c4744b7cefb7fc09dd46909a153cce7773da2" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", "zvariant_utils", ] @@ -3160,5 +3265,5 @@ checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 1.0.109", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index db058cd9f7c..b820ffcc50f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -35,12 +35,12 @@ chrono = { version = "0.4.26", features = ["serde", "std", "clock"], default-fea gethostname = "0.4.3" libc = "0.2.144" tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "8cae9b2a24c65c6c1958f5a0e77d72b23b5c6c30", default-features = false, features = ["connections"] } -keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl"] } +keyring = { version = "2.0.3", default-features = false, features = ["linux-secret-service-rt-tokio-crypto-openssl", "platform-windows", "platform-macos", "linux-keyutils"] } dialoguer = "0.10.4" hyper = { version = "0.14.26", features = ["server", "http1", "runtime"] } indicatif = "0.17.4" tempfile = "3.5.0" -clap_lex = "0.5.0" +clap_lex = "0.7.0" url = "2.3.1" async-trait = "0.1.68" log = "0.4.18" diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 9d5c9b73fdb..67f1bfa6bc7 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -480,6 +480,7 @@ impl Auth { &self, provider: Option, access_token: Option, + refresh_token: Option, ) -> Result { let provider = match provider { Some(p) => p, @@ -490,8 +491,12 @@ impl Auth { Some(t) => StoredCredential { provider, access_token: t, - refresh_token: None, - expires_at: None, + // if a refresh token is given, assume it's valid now but refresh it + // soon in order to get the real expiry time. + expires_at: refresh_token + .as_ref() + .map(|_| Utc::now() + chrono::Duration::minutes(5)), + refresh_token, }, None => self.do_device_code_flow_with_provider(provider).await?, }; diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 1eaaa57353e..79c4d3767a1 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -788,11 +788,14 @@ pub enum TunnelUserSubCommands { #[derive(Args, Debug, Clone)] pub struct LoginArgs { - /// An access token to store for authentication. Note: this will not be - /// refreshed if it expires! - #[clap(long, requires = "provider")] + /// An access token to store for authentication. + #[clap(long, requires = "provider", env = "VSCODE_CLI_ACCESS_TOKEN")] pub access_token: Option, + /// An access token to store for authentication. + #[clap(long, requires = "access_token", env = "VSCODE_CLI_REFRESH_TOKEN")] + pub refresh_token: Option, + /// The auth provider to use. If not provided, a prompt will be shown. #[clap(value_enum, long)] pub provider: Option, diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index f06cd9a1e2a..1755dbbfaef 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -274,10 +274,11 @@ pub async fn service( pub async fn user(ctx: CommandContext, user_args: TunnelUserSubCommands) -> Result { let auth = Auth::new(&ctx.paths, ctx.log.clone()); match user_args { - TunnelUserSubCommands::Login(login_args) => { + TunnelUserSubCommands::Login(mut login_args) => { auth.login( login_args.provider.map(|p| p.into()), - login_args.access_token.to_owned(), + login_args.access_token.take(), + login_args.refresh_token.take(), ) .await?; } @@ -488,7 +489,12 @@ pub async fn forward( forward_args.login.provider.take(), forward_args.login.access_token.take(), ) { - auth.login(Some(p.into()), Some(at)).await?; + auth.login( + Some(p.into()), + Some(at), + forward_args.login.refresh_token.take(), + ) + .await?; } let mut tunnels = DevTunnels::new_port_forwarding(&ctx.log, auth, &ctx.paths); diff --git a/cli/src/rpc.rs b/cli/src/rpc.rs index 0972ad05475..d48d777a5dc 100644 --- a/cli/src/rpc.rs +++ b/cli/src/rpc.rs @@ -634,6 +634,7 @@ const METHOD_STREAMS_STARTED: &str = "streams_started"; const METHOD_STREAM_DATA: &str = "stream_data"; const METHOD_STREAM_ENDED: &str = "stream_ended"; +#[allow(dead_code)] // false positive trait AssertIsSync: Sync {} impl AssertIsSync for RpcDispatcher {} diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index b80a187e266..8a00fda49b8 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -161,7 +161,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index 7672e88e7a4..0e7d733c76f 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -125,10 +125,12 @@ dependencies: "@octokit/openapi-types" "^18.0.0" -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" before-after-hook@^2.2.0: version "2.2.3" @@ -174,6 +176,11 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +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== + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" diff --git a/extensions/cpp/language-configuration.json b/extensions/cpp/language-configuration.json index 3a5459401f9..0bf8df9dc01 100644 --- a/extensions/cpp/language-configuration.json +++ b/extensions/cpp/language-configuration.json @@ -14,7 +14,8 @@ { "open": "(", "close": ")" }, { "open": "'", "close": "'", "notIn": ["string", "comment"] }, { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "/*", "close": "*/", "notIn": ["string", "comment"] } + { "open": "/*", "close": "*/", "notIn": ["string", "comment"] }, + { "open": "/**", "close": " */", "notIn": ["string"] } ], "surroundingPairs": [ ["{", "}"], diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index a0c1107984c..f4f6adfb7f4 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -998,7 +998,7 @@ "vscode-uri": "^3.0.8" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 1b1707150e6..0f1750e800a 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "compile": "gulp compile-extension:css-language-features-server", diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index a30d56158e5..8d4c46d641e 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.19.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.8.tgz#c1e42b165e5a526caf1f010747e0522cb2c9c36a" - integrity sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== dependencies: undici-types "~5.26.4" diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index b8e23eb26ce..25a22d07ca6 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -2,10 +2,12 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" balanced-match@^1.0.0: version "1.0.2" @@ -40,6 +42,11 @@ semver@^7.6.0: dependencies: lru-cache "^6.0.0" +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== + 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" diff --git a/extensions/debug-auto-launch/package.json b/extensions/debug-auto-launch/package.json index 0bc095522a0..4a5d3361f95 100644 --- a/extensions/debug-auto-launch/package.json +++ b/extensions/debug-auto-launch/package.json @@ -33,7 +33,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "prettier": { "printWidth": 100, diff --git a/extensions/debug-auto-launch/yarn.lock b/extensions/debug-auto-launch/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/debug-auto-launch/yarn.lock +++ b/extensions/debug-auto-launch/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/debug-server-ready/package.json b/extensions/debug-server-ready/package.json index 65efcdc09f3..2afe977a9fc 100644 --- a/extensions/debug-server-ready/package.json +++ b/extensions/debug-server-ready/package.json @@ -212,7 +212,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/debug-server-ready/yarn.lock b/extensions/debug-server-ready/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/debug-server-ready/yarn.lock +++ b/extensions/debug-server-ready/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index b7291b9552f..1783bc2ceaf 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -479,7 +479,7 @@ "deps": "yarn add @vscode/emmet-helper" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "dependencies": { "@emmetio/css-parser": "ramya-rao-a/css-parser#vscode", diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index c6d7b12db0e..b75842fe4a4 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -53,10 +53,12 @@ resolved "https://registry.yarnpkg.com/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz#46cffea119a0a003312a21c2d9b5628cb5fcd442" integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI= -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/emmet-helper@^2.8.8": version "2.9.3" @@ -101,6 +103,11 @@ queue@6.0.2: dependencies: inherits "~2.0.3" +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== + vscode-languageserver-textdocument@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.3.tgz#879f2649bfa5a6e07bc8b392c23ede2dfbf43eff" diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index f45105b99d4..184d28e8df0 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -67,7 +67,7 @@ }, "devDependencies": { "@types/markdown-it": "0.0.2", - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index 8bb2a4640df..dd1727edb7b 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -66,7 +66,7 @@ export class ExtensionLinter { private folderToPackageJsonInfo: Record = {}; private packageJsonQ = new Set(); private readmeQ = new Set(); - private timer: NodeJS.Timer | undefined; + private timer: NodeJS.Timeout | undefined; private markdownIt: MarkdownItType.MarkdownIt | undefined; private parse5: typeof import('parse5') | undefined; diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index 5456b3ec040..00fad585fd1 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -7,10 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/node@^6.0.46": version "6.0.78" @@ -66,3 +68,8 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + +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== diff --git a/extensions/git-base/package.json b/extensions/git-base/package.json index c65a8f47e61..3c9b07a13e8 100644 --- a/extensions/git-base/package.json +++ b/extensions/git-base/package.json @@ -104,7 +104,7 @@ ] }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/git-base/yarn.lock b/extensions/git-base/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/git-base/yarn.lock +++ b/extensions/git-base/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/git/package.json b/extensions/git/package.json index 74e09578256..dfbb29289db 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3409,7 +3409,7 @@ "devDependencies": { "@types/byline": "4.2.31", "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/picomatch": "2.3.0", "@types/which": "3.0.0" }, diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 5d3f3c45386..f049939c137 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -192,6 +192,10 @@ export class ApiRepository implements Repository { return this.repository.getRefs(query, cancellationToken); } + checkIgnore(paths: string[]): Promise> { + return this.repository.checkIgnore(paths); + } + getMergeBase(ref1: string, ref2: string): Promise { return this.repository.getMergeBase(ref1, ref2); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index d6d2166e00b..685b5413947 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -235,6 +235,8 @@ export interface Repository { getBranchBase(name: string): Promise; setBranchUpstream(name: string, upstream: string): Promise; + checkIgnore(paths: string[]): Promise>; + getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise; getMergeBase(ref1: string, ref2: string): Promise; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4cfd44bb4f8..ed959765a59 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -3,27 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import TelemetryReporter from '@vscode/extension-telemetry'; import * as fs from 'fs'; import * as path from 'path'; import * as picomatch from 'picomatch'; -import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, TabInputTextDiff, TabInputNotebookDiff, TabInputTextMultiDiff, RelativePattern, CancellationTokenSource, LogOutputChannel, LogLevel, CancellationError, l10n } from 'vscode'; -import TelemetryReporter from '@vscode/extension-telemetry'; -import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, Remote, Status, CommitOptions, BranchQuery, FetchOptions, RefQuery, RefType } from './api/git'; +import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, Uri, window, workspace, WorkspaceEdit } from 'vscode'; +import { ActionButton } from './actionButton'; +import { ApiRepository } from './api/api1'; +import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefQuery, RefType, Remote, Status } from './api/git'; import { AutoFetcher } from './autofetch'; +import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; import { debounce, memoize, throttle } from './decorators'; -import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions, LsTreeElement } from './git'; +import { Repository as BaseRepository, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, Stash, Submodule } from './git'; +import { GitHistoryProvider } from './historyProvider'; +import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; +import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands'; +import { IPushErrorHandlerRegistry } from './pushError'; +import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; -import { IPushErrorHandlerRegistry } from './pushError'; -import { ApiRepository } from './api/api1'; -import { IRemoteSourcePublisherRegistry } from './remotePublisher'; -import { ActionButton } from './actionButton'; -import { IPostCommitCommandsProviderRegistry, CommitCommandsCenter } from './postCommitCommands'; -import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; -import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; -import { GitHistoryProvider } from './historyProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 219c87b148d..eac6f0384b9 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -81,7 +81,7 @@ export function onceEvent(event: Event): Event { export function debounceEvent(event: Event, delay: number): Event { return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => { - let timer: NodeJS.Timer; + let timer: NodeJS.Timeout; return event(e => { clearTimeout(timer); timer = setTimeout(() => listener.call(thisArgs, e), delay); diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index dfb24f6e7d2..266157e9e5c 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -122,10 +122,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/picomatch@2.3.0": version "2.3.0" @@ -239,6 +241,11 @@ token-types@^4.1.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.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== + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index d55e8dcfd03..2d2bea56277 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -65,7 +65,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/node-fetch": "^2.5.7" }, "repository": { diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 3d73bfb7656..15fe2ef04f8 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 { CANCELLATION_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; interface SessionData { id: string; @@ -298,51 +298,23 @@ 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' }); const scopeString = sortedScopes.join(' '); - const existingLogin = sessions[0]?.account.label; const token = await this._githubServer.login(scopeString, existingLogin); const session = await this.tokenToSession(token, scopes); this.afterSessionLoad(session); - if (sessions.some(s => s.account.id !== session.account.id)) { - const otherAccountsIndexes = new Array(); - const otherAccountsLabels = new Set(); - for (let i = 0; i < sessions.length; i++) { - if (sessions[i].account.id !== session.account.id) { - otherAccountsIndexes.push(i); - otherAccountsLabels.add(sessions[i].account.label); - } - } - const proceed = vscode.l10n.t("Continue"); - const labelstr = [...otherAccountsLabels].join(', '); - const result = await vscode.window.showInformationMessage( - vscode.l10n.t({ - message: "You are logged into another account already ({0}).\n\nDo you want to log out of that account and log in to '{1}' instead?", - comment: ['{0} is a comma-separated list of account names. {1} is the account name to log into.'], - args: [labelstr, session.account.label] - }), - { modal: true }, - proceed - ); - if (result !== proceed) { - throw new Error(CANCELLATION_ERROR); - } - - // Remove other accounts - for (const i of otherAccountsIndexes) { - sessions.splice(i, 1); - } - } - const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); + const removed = new Array(); if (sessionIndex > -1) { - sessions.splice(sessionIndex, 1, session); + removed.push(...sessions.splice(sessionIndex, 1, session)); } else { sessions.push(session); } await this.storeSessions(sessions); - this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); + this._sessionChangeEmitter.fire({ added: [session], removed, changed: [] }); this._logger.info('Login success!'); diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index 724b304c53e..8ef2192404a 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -113,10 +113,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -182,6 +184,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +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== + vscode-tas-client@^0.1.84: version "0.1.84" resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz#906bdcfd8c9e1dc04321d6bc0335184f9119968e" diff --git a/extensions/github/package.json b/extensions/github/package.json index 5596a2c0557..ece19e32f54 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -186,7 +186,7 @@ "@vscode/extension-telemetry": "^0.9.0" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock index caf35ace98a..912a28439be 100644 --- a/extensions/github/yarn.lock +++ b/extensions/github/yarn.lock @@ -225,10 +225,12 @@ dependencies: "@octokit/openapi-types" "^17.1.0" -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -295,6 +297,11 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +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== + universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index 6869f9ce506..ae533cc0e47 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -19,7 +19,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/grunt/yarn.lock b/extensions/grunt/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/grunt/yarn.lock +++ b/extensions/grunt/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/gulp/package.json b/extensions/gulp/package.json index 3e29c75fe4d..0c19b688477 100644 --- a/extensions/gulp/package.json +++ b/extensions/gulp/package.json @@ -18,7 +18,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/gulp/yarn.lock b/extensions/gulp/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/gulp/yarn.lock +++ b/extensions/gulp/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index dd984454949..49489ff20df 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -263,7 +263,7 @@ "vscode-uri": "^3.0.8" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 0e3ec8667ab..75bfa00de11 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "compile": "npx gulp compile-extension:html-language-features-server", diff --git a/extensions/html-language-features/server/src/languageModelCache.ts b/extensions/html-language-features/server/src/languageModelCache.ts index 048d84d37cd..5dd8e439f5c 100644 --- a/extensions/html-language-features/server/src/languageModelCache.ts +++ b/extensions/html-language-features/server/src/languageModelCache.ts @@ -15,7 +15,7 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime let languageModels: { [uri: string]: { version: number; languageId: string; cTime: number; languageModel: T } } = {}; let nModels = 0; - let cleanupInterval: NodeJS.Timer | undefined = undefined; + let cleanupInterval: NodeJS.Timeout | undefined = undefined; if (cleanupIntervalTimeInSec > 0) { cleanupInterval = setInterval(() => { const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index c8ba196769b..f327f1f352f 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -7,16 +7,23 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.12.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3" + integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw== + dependencies: + undici-types "~5.26.4" "@vscode/l10n@^0.0.18": version "0.0.18" resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.18.tgz#916d3a5e960dbab47c1c56f58a7cb5087b135c95" integrity sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ== +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== + 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" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index b8072052af1..d1d73407809 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -142,6 +144,11 @@ semver@^7.6.0: dependencies: lru-cache "^6.0.0" +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== + 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" diff --git a/extensions/jake/package.json b/extensions/jake/package.json index 637d417e503..1d5d1250db0 100644 --- a/extensions/jake/package.json +++ b/extensions/jake/package.json @@ -18,7 +18,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/jake/yarn.lock b/extensions/jake/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/jake/yarn.lock +++ b/extensions/jake/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 7df9aa20be8..f86470429a4 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -166,7 +166,7 @@ "vscode-languageclient": "^10.0.0-next.5" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 46e8f0d94dd..6134fb4224d 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "prepublishOnly": "npm run clean && npm run compile", diff --git a/extensions/json-language-features/server/src/languageModelCache.ts b/extensions/json-language-features/server/src/languageModelCache.ts index 17ffe2add4f..441a5a19b28 100644 --- a/extensions/json-language-features/server/src/languageModelCache.ts +++ b/extensions/json-language-features/server/src/languageModelCache.ts @@ -15,7 +15,7 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime let languageModels: { [uri: string]: { version: number; languageId: string; cTime: number; languageModel: T } } = {}; let nModels = 0; - let cleanupInterval: NodeJS.Timer | undefined = undefined; + let cleanupInterval: NodeJS.Timeout | undefined = undefined; if (cleanupIntervalTimeInSec > 0) { cleanupInterval = setInterval(() => { const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; @@ -79,4 +79,4 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime } } }; -} \ No newline at end of file +} diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 343feaa178f..669e823497d 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -7,10 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.12.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3" + integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw== + dependencies: + undici-types "~5.26.4" "@vscode/l10n@^0.0.18": version "0.0.18" @@ -27,6 +29,11 @@ request-light@^0.7.0: resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.7.0.tgz#885628bb2f8040c26401ebf258ec51c4ae98ac2a" integrity sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q== +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== + 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" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index 8374cca5b83..b7ca937103a 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -147,6 +149,11 @@ semver@^7.6.0: dependencies: lru-cache "^6.0.0" +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== + 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" diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json index df9124c936b..532c2dec843 100644 --- a/extensions/markdown-language-features/server/package.json +++ b/extensions/markdown-language-features/server/package.json @@ -22,7 +22,7 @@ "vscode-uri": "^3.0.7" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "scripts": { "compile": "gulp compile-extension:markdown-language-features-server", diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock index 0768663fe0d..148783435bd 100644 --- a/extensions/markdown-language-features/server/yarn.lock +++ b/extensions/markdown-language-features/server/yarn.lock @@ -2,10 +2,12 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/l10n@^0.0.10": version "0.0.10" @@ -98,6 +100,11 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +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== + vscode-jsonrpc@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94" diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index fc232bfeecb..4ab245c192b 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -20,6 +20,7 @@ enum MediaKind { export const mediaFileExtensions = new Map([ // Images + ['avif', MediaKind.Image], ['bmp', MediaKind.Image], ['gif', MediaKind.Image], ['ico', MediaKind.Image], diff --git a/extensions/markdown-language-features/src/util/mimes.ts b/extensions/markdown-language-features/src/util/mimes.ts index 8028294b3f4..f33b807b83e 100644 --- a/extensions/markdown-language-features/src/util/mimes.ts +++ b/extensions/markdown-language-features/src/util/mimes.ts @@ -9,6 +9,7 @@ export const Mime = { } as const; export const mediaMimes = new Set([ + 'image/avif', 'image/bmp', 'image/gif', 'image/jpeg', diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index f55fb26c44e..cdda46fab32 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -169,7 +169,7 @@ "@vscode/extension-telemetry": "^0.9.0" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/merge-conflict/yarn.lock b/extensions/merge-conflict/yarn.lock index f93736b6b27..31f7cee0830 100644 --- a/extensions/merge-conflict/yarn.lock +++ b/extensions/merge-conflict/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@vscode/extension-telemetry@^0.9.0": version "0.9.0" @@ -108,3 +110,8 @@ "@microsoft/1ds-core-js" "^4.0.3" "@microsoft/1ds-post-js" "^4.0.3" "@microsoft/applicationinsights-web-basic" "^3.0.4" + +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== diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index c82eea19318..3d73a7621fd 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -109,7 +109,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "devDependencies": { - "@types/node": "18.x", + "@types/node": "20.x", "@types/node-fetch": "^2.5.7", "@types/randombytes": "^2.0.0", "@types/sha.js": "^2.4.0", diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index afa82e5759f..6f277110a56 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -113,10 +113,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/randombytes@^2.0.0": version "2.0.0" @@ -196,6 +198,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +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== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 37411d32e52..545ce102ab5 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "@types/minimatch": "^5.1.2", - "@types/node": "18.x", + "@types/node": "20.x", "@types/which": "^3.0.0" }, "main": "./out/npmMain", diff --git a/extensions/npm/src/features/packageJSONContribution.ts b/extensions/npm/src/features/packageJSONContribution.ts index 03913d0a8de..a2f4fabcfe3 100644 --- a/extensions/npm/src/features/packageJSONContribution.ts +++ b/extensions/npm/src/features/packageJSONContribution.ts @@ -292,7 +292,7 @@ export class PackageJSONContribution implements IJSONContribution { // COREPACK_ENABLE_AUTO_PIN disables the package.json overwrite, and // COREPACK_ENABLE_PROJECT_SPEC makes the npm view command succeed // even if packageManager specified a package manager other than npm. - const env = { COREPACK_ENABLE_AUTO_PIN: "0", COREPACK_ENABLE_PROJECT_SPEC: "0" }; + const env = { ...process.env, COREPACK_ENABLE_AUTO_PIN: '0', COREPACK_ENABLE_PROJECT_SPEC: '0' }; cp.execFile(npmCommandPath, args, { cwd, env }, (error, stdout) => { if (!error) { try { diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index 7dad0575479..a7afc9f801f 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -7,10 +7,12 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/which@^3.0.0": version "3.0.0" @@ -181,6 +183,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +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== + vscode-uri@^3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" diff --git a/extensions/package.json b/extensions/package.json index ef529f4ae68..2c83af40936 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -13,5 +13,8 @@ "@parcel/watcher": "2.1.0", "esbuild": "0.20.0", "vscode-grammar-updater": "^1.1.0" + }, + "resolutions": { + "node-gyp-build": "4.8.1" } } diff --git a/extensions/php-language-features/package.json b/extensions/php-language-features/package.json index 8963fc796f4..989213b6c0c 100644 --- a/extensions/php-language-features/package.json +++ b/extensions/php-language-features/package.json @@ -77,7 +77,7 @@ "which": "^2.0.2" }, "devDependencies": { - "@types/node": "18.x", + "@types/node": "20.x", "@types/which": "^2.0.0" }, "repository": { diff --git a/extensions/php-language-features/yarn.lock b/extensions/php-language-features/yarn.lock index 4c2e01e4b71..ea9947b69e3 100644 --- a/extensions/php-language-features/yarn.lock +++ b/extensions/php-language-features/yarn.lock @@ -2,10 +2,12 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/which@^2.0.0": version "2.0.0" @@ -17,6 +19,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= +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== + which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" diff --git a/extensions/references-view/package.json b/extensions/references-view/package.json index 228332773c6..9566a965c76 100644 --- a/extensions/references-view/package.json +++ b/extensions/references-view/package.json @@ -399,6 +399,6 @@ "watch": "npx gulp watch-extension:references-view" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" } } diff --git a/extensions/references-view/yarn.lock b/extensions/references-view/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/references-view/yarn.lock +++ b/extensions/references-view/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index d6703a73704..57578ca9692 100644 --- a/extensions/theme-defaults/themes/dark_modern.json +++ b/extensions/theme-defaults/themes/dark_modern.json @@ -100,6 +100,7 @@ "tab.activeBorder": "#1F1F1F", "tab.activeBorderTop": "#0078D4", "tab.activeForeground": "#FFFFFF", + "tab.selectedBorderTop": "#6caddf", "tab.border": "#2B2B2B", "tab.hoverBackground": "#1F1F1F", "tab.inactiveBackground": "#181818", diff --git a/extensions/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index 543e4efcecd..331a87bb778 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -22,6 +22,8 @@ "ports.iconRunningProcessForeground": "#369432", "sideBarSectionHeader.background": "#0000", "sideBarSectionHeader.border": "#ccc3", + "tab.selectedBackground": "#222222", + "tab.selectedForeground": "#ffffffa0", "tab.lastPinnedBorder": "#ccc3", "list.activeSelectionIconForeground": "#FFF", "terminal.inactiveSelectionBackground": "#3A3D41", diff --git a/extensions/theme-defaults/themes/light_modern.json b/extensions/theme-defaults/themes/light_modern.json index d39d41eee22..bd7e647afb3 100644 --- a/extensions/theme-defaults/themes/light_modern.json +++ b/extensions/theme-defaults/themes/light_modern.json @@ -116,6 +116,7 @@ "tab.activeBorder": "#F8F8F8", "tab.activeBorderTop": "#005FB8", "tab.activeForeground": "#3B3B3B", + "tab.selectedBorderTop": "#68a3da", "tab.border": "#E5E5E5", "tab.hoverBackground": "#FFFFFF", "tab.inactiveBackground": "#F8F8F8", @@ -134,7 +135,7 @@ "textLink.activeForeground": "#005FB8", "textLink.foreground": "#005FB8", "textPreformat.foreground": "#3B3B3B", - "textPreformat.background": "#0000001F", + "textPreformat.background": "#0000001F", "textSeparator.foreground": "#21262D", "titleBar.activeBackground": "#F8F8F8", "titleBar.activeForeground": "#1E1E1E", diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index bdd063fbc56..e4cc701f82c 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -23,6 +23,8 @@ "ports.iconRunningProcessForeground": "#369432", "sideBarSectionHeader.background": "#0000", "sideBarSectionHeader.border": "#61616130", + "tab.selectedForeground": "#333333b3", + "tab.selectedBackground": "#ffffffa5", "tab.lastPinnedBorder": "#61616130", "notebook.cellBorderColor": "#E8E8E8", "notebook.selectedCellBackground": "#c8ddf150", diff --git a/extensions/tunnel-forwarding/package.json b/extensions/tunnel-forwarding/package.json index 76c61e4dad9..315baa03598 100644 --- a/extensions/tunnel-forwarding/package.json +++ b/extensions/tunnel-forwarding/package.json @@ -44,7 +44,7 @@ "watch": "gulp watch-extension:tunnel-forwarding" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "prettier": { "printWidth": 100, diff --git a/extensions/tunnel-forwarding/src/extension.ts b/extensions/tunnel-forwarding/src/extension.ts index 09e0d447cdb..3dc88224aaa 100644 --- a/extensions/tunnel-forwarding/src/extension.ts +++ b/extensions/tunnel-forwarding/src/extension.ts @@ -260,12 +260,10 @@ class TunnelProvider implements vscode.TunnelProvider { 'forward-internal', '--provider', 'github', - '--access-token', - session.accessToken, ]; this.logger.log('info', '[forwarding] starting CLI'); - const child = spawn(cliPath, args, { stdio: 'pipe', env: { ...process.env, NO_COLOR: '1' } }); + const child = spawn(cliPath, args, { stdio: 'pipe', env: { ...process.env, NO_COLOR: '1', VSCODE_CLI_ACCESS_TOKEN: session.accessToken } }); this.state = { state: State.Starting, process: child }; const progressP = new DeferredPromise(); diff --git a/extensions/tunnel-forwarding/yarn.lock b/extensions/tunnel-forwarding/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/tunnel-forwarding/yarn.lock +++ b/extensions/tunnel-forwarding/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index fb5383a2978..e01c9c96527 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -50,7 +50,7 @@ "vscode-uri": "^3.0.3" }, "devDependencies": { - "@types/node": "18.x", + "@types/node": "20.x", "@types/semver": "^5.5.0" }, "scripts": { @@ -140,6 +140,10 @@ "fileMatch": "jsconfig.*.json", "url": "./schemas/jsconfig.schema.json" }, + { + "fileMatch": ".swcrc", + "url": "https://swc.rs/schema.json" + }, { "fileMatch": "typedoc.json", "url": "https://typedoc.org/schema.json" @@ -1280,22 +1284,21 @@ }, "typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors": { "type": "boolean", - "default": true, + "default": false, "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", "scope": "window" }, + "typescript.tsserver.web.typeAcquisition.enabled": { + "type": "boolean", + "default": true, + "description": "%configuration.tsserver.web.typeAcquisition.enabled%", + "scope": "window" + }, "typescript.tsserver.nodePath": { "type": "string", "description": "%configuration.tsserver.nodePath%", "scope": "window" }, - "typescript.experimental.tsserver.web.typeAcquisition.enabled": { - "type": "boolean", - "default": false, - "description": "%configuration.experimental.tsserver.web.typeAcquisition.enabled%", - "scope": "window", - "tags": ["experimental"] - }, "typescript.preferGoToSourceDefinition": { "type": "boolean", "default": false, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index e60451eaeb4..7fb5bae6ad1 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -216,9 +216,9 @@ "configuration.suggest.classMemberSnippets.enabled": "Enable/disable snippet completions for class members.", "configuration.suggest.objectLiteralMethodSnippets.enabled": "Enable/disable snippet completions for methods in object literals.", "configuration.tsserver.web.projectWideIntellisense.enabled": "Enable/disable project-wide IntelliSense on web. Requires that VS Code is running in a trusted context.", - "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors. This is needed when using external packages as these can't be included analyzed on web.", + "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.experimental.tsserver.web.typeAcquisition.enabled": "Enable/disable package acquisition on the web.", "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 47620bd0743..2f0ff4b0a28 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -112,7 +112,7 @@ export interface TypeScriptServiceConfiguration { readonly useSyntaxServer: SyntaxServerConfiguration; readonly webProjectWideIntellisenseEnabled: boolean; readonly webProjectWideIntellisenseSuppressSemanticErrors: boolean; - readonly webExperimentalTypeAcquisition: boolean; + readonly webTypeAcquisitionEnabled: boolean; readonly enableDiagnosticsTelemetry: boolean; readonly enableProjectDiagnostics: boolean; readonly maxTsServerMemory: number; @@ -150,7 +150,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu useSyntaxServer: this.readUseSyntaxServer(configuration), webProjectWideIntellisenseEnabled: this.readWebProjectWideIntellisenseEnable(configuration), webProjectWideIntellisenseSuppressSemanticErrors: this.readWebProjectWideIntellisenseSuppressSemanticErrors(configuration), - webExperimentalTypeAcquisition: this.readWebExperimentalTypeAcquisition(configuration), + webTypeAcquisitionEnabled: this.readWebTypeAcquisition(configuration), enableDiagnosticsTelemetry: this.readEnableDiagnosticsTelemetry(configuration), enableProjectDiagnostics: this.readEnableProjectDiagnostics(configuration), maxTsServerMemory: this.readMaxTsServerMemory(configuration), @@ -187,10 +187,6 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return configuration.get('typescript.disableAutomaticTypeAcquisition', false); } - protected readWebExperimentalTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.experimental.tsserver.web.typeAcquisition.enabled', false); - } - protected readLocale(configuration: vscode.WorkspaceConfiguration): string | null { const value = configuration.get('typescript.locale', 'auto'); return !value || value === 'auto' ? null : value; @@ -256,15 +252,19 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return configuration.get('typescript.tsserver.enableTracing', false); } + private readWorkspaceSymbolsExcludeLibrarySymbols(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.workspaceSymbols.excludeLibrarySymbols', true); + } + private readWebProjectWideIntellisenseEnable(configuration: vscode.WorkspaceConfiguration): boolean { return configuration.get('typescript.tsserver.web.projectWideIntellisense.enabled', true); } private readWebProjectWideIntellisenseSuppressSemanticErrors(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors', true); + return configuration.get('typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors', false); } - private readWorkspaceSymbolsExcludeLibrarySymbols(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.workspaceSymbols.excludeLibrarySymbols', true); + private readWebTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.tsserver.web.typeAcquisition.enabled', true); } } diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index 91a652ed6fa..2f6bd2127e1 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -25,7 +25,7 @@ import { ITypeScriptVersionProvider, TypeScriptVersion, TypeScriptVersionSource import { ActiveJsTsEditorTracker } from './ui/activeJsTsEditorTracker'; import { Disposable } from './utils/dispose'; import { getPackageInfo } from './utils/packageInfo'; -import { isWebAndHasSharedArrayBuffers } from './utils/platform'; +import { isWebAndHasSharedArrayBuffers, supportsReadableByteStreams } from './utils/platform'; class StaticVersionProvider implements ITypeScriptVersionProvider { @@ -62,7 +62,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { new TypeScriptVersion( TypeScriptVersionSource.Bundled, vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript/tsserver.web.js').toString(), - API.fromSimpleString('5.3.2'))); + API.fromSimpleString('5.4.5'))); let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined; const packageInfo = getPackageInfo(context); @@ -101,14 +101,17 @@ export async function activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push(lazilyActivateClient(lazyClientHost, pluginManager, activeJsTsEditorTracker, async () => { await startPreloadWorkspaceContentsIfNeeded(context, logger); })); - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs(), { - isCaseSensitive: true, - isReadonly: false - })); - context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(), { - isCaseSensitive: true, - isReadonly: false - })); + + if (supportsReadableByteStreams()) { + context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs(), { + isCaseSensitive: true, + isReadonly: false + })); + context.subscriptions.push(vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(), { + isCaseSensitive: true, + isReadonly: false + })); + } return getExtensionApi(onCompletionAccepted.event, pluginManager); } @@ -118,15 +121,25 @@ async function startPreloadWorkspaceContentsIfNeeded(context: vscode.ExtensionCo return; } - const workspaceUri = vscode.workspace.workspaceFolders?.at(0)?.uri; - if (!workspaceUri || workspaceUri.scheme !== 'vscode-vfs' || !workspaceUri.authority.startsWith('github')) { - logger.info(`Skipped loading workspace contents for repository ${workspaceUri?.toString()}`); + if (!vscode.workspace.workspaceFolders) { return; } - const loader = new RemoteWorkspaceContentsPreloader(workspaceUri, logger); - context.subscriptions.push(loader); - return loader.triggerPreload(); + await Promise.all(vscode.workspace.workspaceFolders.map(async folder => { + const workspaceUri = folder.uri; + if (workspaceUri.scheme !== 'vscode-vfs' || !workspaceUri.authority.startsWith('github')) { + logger.info(`Skipped pre loading workspace contents for repository ${workspaceUri?.toString()}`); + return; + } + + const loader = new RemoteWorkspaceContentsPreloader(workspaceUri, logger); + context.subscriptions.push(loader); + try { + await loader.triggerPreload(); + } catch (error) { + console.error(error); + } + })); } class RemoteWorkspaceContentsPreloader extends Disposable { diff --git a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts index 450ffcb886c..190e6a99bf7 100644 --- a/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/typescript-language-features/src/languageFeatures/diagnostics.ts @@ -156,7 +156,7 @@ class DiagnosticsTelemetryManager extends Disposable { private readonly _diagnosticCodesMap = new Map(); private readonly _diagnosticSnapshotsMap = new ResourceMap(uri => uri.toString(), { onCaseInsensitiveFileSystem: false }); private _timeout: NodeJS.Timeout | undefined; - private _telemetryEmitter: NodeJS.Timer | undefined; + private _telemetryEmitter: NodeJS.Timeout | undefined; constructor( private readonly _telemetryReporter: TelemetryReporter, diff --git a/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts b/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts index 0d04d2dc808..06ce5557b6c 100644 --- a/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts +++ b/extensions/typescript-language-features/src/languageFeatures/mappedCodeEditProvider.ts @@ -27,8 +27,8 @@ class TsMappedEditsProvider implements vscode.MappedEditsProvider { } const response = await this.client.execute('mapCode', { - mappings: [{ - file, + file, + mapping: { contents: codeBlocks, focusLocations: context.documents.map(documents => { return documents.flatMap((contextItem): FileSpan[] => { @@ -39,7 +39,7 @@ class TsMappedEditsProvider implements vscode.MappedEditsProvider { return contextItem.ranges.map((range): FileSpan => ({ file, ...Range.toTextSpan(range) })); }); }), - }], + } }, token); if (response.type !== 'response' || !response.body) { return; diff --git a/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts b/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts index 36c894e8e1e..45ac08e14a4 100644 --- a/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts +++ b/extensions/typescript-language-features/src/languageFeatures/tagClosing.ts @@ -17,7 +17,7 @@ class TagClosing extends Disposable { public static readonly minVersion = API.v300; private _disposed = false; - private _timeout: NodeJS.Timer | undefined = undefined; + private _timeout: NodeJS.Timeout | undefined = undefined; private _cancel: vscode.CancellationTokenSource | undefined = undefined; constructor( diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 6157c3b8cb4..b09df40561b 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -9,6 +9,7 @@ import { CommandManager } from './commands/commandManager'; import { DocumentSelector } from './configuration/documentSelector'; import * as fileSchemes from './configuration/fileSchemes'; import { LanguageDescription } from './configuration/languageDescription'; +import { Schemes } from './configuration/schemes'; import { DiagnosticKind } from './languageFeatures/diagnostics'; import FileConfigurationManager from './languageFeatures/fileConfigurationManager'; import { TelemetryReporter } from './logging/telemetry'; @@ -17,7 +18,7 @@ import { ClientCapability } from './typescriptService'; import TypeScriptServiceClient from './typescriptServiceClient'; import TypingsStatus from './ui/typingsStatus'; import { Disposable } from './utils/dispose'; -import { isWeb } from './utils/platform'; +import { isWeb, isWebAndHasSharedArrayBuffers, supportsReadableByteStreams } from './utils/platform'; const validateSetting = 'validate.enable'; @@ -141,7 +142,19 @@ export default class LanguageProvider extends Disposable { return; } - if (diagnosticsKind === DiagnosticKind.Semantic && isWeb() && this.client.configuration.webProjectWideIntellisenseSuppressSemanticErrors) { + if (diagnosticsKind === DiagnosticKind.Semantic && isWeb()) { + if ( + !isWebAndHasSharedArrayBuffers() + || !supportsReadableByteStreams() // No ata. Will result in lots of false positives + || this.client.configuration.webProjectWideIntellisenseSuppressSemanticErrors + || !this.client.configuration.webProjectWideIntellisenseEnabled + ) { + return; + } + } + + // Disable semantic errors in notebooks until we have better notebook support + if (diagnosticsKind === DiagnosticKind.Semantic && file.scheme === Schemes.notebookCell) { return; } diff --git a/extensions/typescript-language-features/src/tsServer/api.ts b/extensions/typescript-language-features/src/tsServer/api.ts index 4a35ada0f24..2378bfb53f0 100644 --- a/extensions/typescript-language-features/src/tsServer/api.ts +++ b/extensions/typescript-language-features/src/tsServer/api.ts @@ -80,4 +80,8 @@ export class API { public lt(other: API): boolean { return !this.gte(other); } + + public isYarnPnp(): boolean { + return this.fullVersionString.includes('-sdk'); + } } 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 45e09d63481..aa9b0589d2d 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/protocol.d.ts @@ -20,42 +20,36 @@ declare module '../../../../node_modules/typescript/lib/typescript' { readonly _serverType?: ServerType; } - export interface MapCodeRequestArgs { - /// The files and changes to try and apply/map. - mappings: MapCodeRequestDocumentMapping[]; - - /// Edits to apply to the current workspace before performing the mapping. - updates?: FileCodeEdits[] + export interface MapCodeRequestArgs extends FileRequestArgs { + /** + * The files and changes to try and apply/map. + */ + mapping: MapCodeRequestDocumentMapping; } export interface MapCodeRequestDocumentMapping { - /// The file for the request (absolute pathname required). Null/undefined - /// if specific file is unknown. - file?: string; - - /// Optional name of project that contains file - projectFileName?: string; - - /// The specific code to map/insert/replace in the file. + /** + * 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?: FileSpan[][]; + /** + * 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 Request { - command: 'mapCode', + export interface MapCodeRequest extends FileRequest { + command: 'mapCode'; arguments: MapCodeRequestArgs; } export interface MapCodeResponse extends Response { - body: FileCodeEdits[] + body: readonly FileCodeEdits[]; } } } - - diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts index bb57c2644b4..71daf1fb0b6 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts @@ -8,12 +8,13 @@ import { ApiService, Requests } from '@vscode/sync-api-service'; import * as vscode from 'vscode'; import { TypeScriptServiceConfiguration } from '../configuration/configuration'; import { Logger } from '../logging/logger'; +import { supportsReadableByteStreams } from '../utils/platform'; import { FileWatcherManager } from './fileWatchingManager'; +import { NodeVersionManager } from './nodeManager'; import type * as Proto from './protocol/protocol'; import { TsServerLog, TsServerProcess, TsServerProcessFactory, TsServerProcessKind } from './server'; import { TypeScriptVersionManager } from './versionManager'; import { TypeScriptVersion } from './versionProvider'; -import { NodeVersionManager } from './nodeManager'; type BrowserWatchEvent = { type: 'watchDirectory' | 'watchFile'; @@ -50,7 +51,7 @@ export class WorkerServerProcessFactory implements TsServerProcessFactory { // Explicitly give TS Server its path so it can load local resources '--executingFilePath', tsServerPath, ]; - if (_configuration.webExperimentalTypeAcquisition) { + if (_configuration.webTypeAcquisitionEnabled && supportsReadableByteStreams()) { launchArgs.push('--experimentalTypeAcquisition'); } return new WorkerServerProcess(kind, tsServerPath, this._extensionUri, launchArgs, tsServerLog, this._logger); diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index b88251a7033..543140dbab5 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -234,7 +234,7 @@ export class TypeScriptServerSpawner { tsServerLog = { type: 'file', uri: logFilePath }; args.push('--logVerbosity', TsServerLogLevel.toString(configuration.tsServerLogLevel)); - args.push('--logFile', logFilePath.fsPath); + args.push('--logFile', `"${logFilePath.fsPath}"`); } } } @@ -242,7 +242,7 @@ export class TypeScriptServerSpawner { if (configuration.enableTsServerTracing && !isWeb()) { tsServerTraceDirectory = this._logDirectoryProvider.getNewLogDirectory(); if (tsServerTraceDirectory) { - args.push('--traceDirectory', tsServerTraceDirectory.fsPath); + args.push('--traceDirectory', `"${tsServerTraceDirectory.fsPath}"`); } } @@ -271,7 +271,11 @@ export class TypeScriptServerSpawner { args.push('--noGetErrOnBackgroundUpdate'); - if (apiVersion.gte(API.v544) && configuration.useVsCodeWatcher) { + if ( + apiVersion.gte(API.v544) + && configuration.useVsCodeWatcher + && !apiVersion.isYarnPnp() // Disable for yarn pnp as it currently breaks with the VS Code watcher + ) { args.push('--canUseWatchEvents'); } diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 78a77f23c9f..24742f99219 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -5,32 +5,32 @@ import * as path from 'path'; import * as vscode from 'vscode'; +import { ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration, areServiceConfigurationsEqual } from './configuration/configuration'; +import * as fileSchemes from './configuration/fileSchemes'; +import { Schemes } from './configuration/schemes'; import { IExperimentationTelemetryReporter } from './experimentTelemetryReporter'; import { DiagnosticKind, DiagnosticsManager } from './languageFeatures/diagnostics'; -import * as Proto from './tsServer/protocol/protocol'; -import { EventName } from './tsServer/protocol/protocol.const'; +import { Logger } from './logging/logger'; +import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './logging/telemetry'; +import Tracer from './logging/tracer'; +import { ProjectType, inferredProjectCompilerOptions } from './tsconfig'; import { API } from './tsServer/api'; import BufferSyncSupport from './tsServer/bufferSyncSupport'; import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; +import { NodeVersionManager } from './tsServer/nodeManager'; import { TypeScriptPluginPathsProvider } from './tsServer/pluginPathsProvider'; +import { PluginManager, TypeScriptServerPlugin } from './tsServer/plugins'; +import * as Proto from './tsServer/protocol/protocol'; +import { EventName } from './tsServer/protocol/protocol.const'; import { ITypeScriptServer, TsServerLog, TsServerProcessFactory, TypeScriptServerExitEvent } from './tsServer/server'; import { TypeScriptServerError } from './tsServer/serverError'; import { TypeScriptServerSpawner } from './tsServer/spawner'; import { TypeScriptVersionManager } from './tsServer/versionManager'; import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider'; import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; -import { ServiceConfigurationProvider, SyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration, areServiceConfigurationsEqual } from './configuration/configuration'; import { Disposable, DisposableStore, disposeAll } from './utils/dispose'; -import * as fileSchemes from './configuration/fileSchemes'; -import { Logger } from './logging/logger'; import { isWeb, isWebAndHasSharedArrayBuffers } from './utils/platform'; -import { PluginManager, TypeScriptServerPlugin } from './tsServer/plugins'; -import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './logging/telemetry'; -import Tracer from './logging/tracer'; -import { ProjectType, inferredProjectCompilerOptions } from './tsconfig'; -import { Schemes } from './configuration/schemes'; -import { NodeVersionManager } from './tsServer/nodeManager'; export interface TsDiagnostics { @@ -463,7 +463,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType } */ this.logTelemetry('tsserver.error'); - this.serviceExited(false); + this.serviceExited(false, apiVersion); }); handle.onExit((data: TypeScriptServerExitEvent) => { @@ -484,7 +484,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType */ this.logTelemetry('tsserver.exitWithCode', { code: code ?? undefined, signal: signal ?? undefined }); - if (this.token !== mytoken) { // this is coming from an old process return; @@ -493,7 +492,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType if (handle.tsServerLog?.type === 'file') { this.info(`TSServer log file: ${handle.tsServerLog.uri.fsPath}`); } - this.serviceExited(!this.isRestarting); + this.serviceExited(!this.isRestarting, apiVersion); this.isRestarting = false; }); @@ -612,7 +611,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType }; } - private serviceExited(restart: boolean): void { + private serviceExited(restart: boolean, tsVersion: API): void { this.resetWatchers(); this.loadingIndicator.reset(); @@ -686,17 +685,34 @@ export default class TypeScriptServiceClient extends Disposable implements IType this._isPromptingAfterCrash = true; } - prompt?.then(item => { + prompt?.then(async item => { this._isPromptingAfterCrash = false; if (item === reportIssueItem) { + const minModernTsVersion = this.versionProvider.bundledVersion.apiVersion; - if ( + // Don't allow reporting issues using the PnP patched version of TS Server + if (tsVersion.isYarnPnp()) { + const reportIssue: vscode.MessageItem = { + title: vscode.l10n.t("Report issue against Yarn PnP"), + }; + const response = await vscode.window.showWarningMessage( + vscode.l10n.t("Please report an issue against Yarn PnP"), + { + modal: true, + detail: vscode.l10n.t("The workspace is using a version of the TypeScript Server that has been patched by Yarn PnP. This patching is a common source of bugs."), + }, + reportIssue); + + if (response === reportIssue) { + vscode.env.openExternal(vscode.Uri.parse('https://github.com/yarnpkg/berry/issues')); + } + } + // Don't allow reporting issues with old TS versions + else if ( minModernTsVersion && - previousState.type === ServerState.Type.Errored && - previousState.error instanceof TypeScriptServerError && - previousState.error.version.apiVersion?.lt(minModernTsVersion) + tsVersion.lt(minModernTsVersion) ) { vscode.window.showWarningMessage( vscode.l10n.t("Please update your TypeScript version"), @@ -704,7 +720,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType modal: true, detail: vscode.l10n.t( "The workspace is using an old version of TypeScript ({0}).\n\nBefore reporting an issue, please update the workspace to use TypeScript {1} or newer to make sure the bug has not already been fixed.", - previousState.error.version.apiVersion.displayName, + tsVersion.displayName, minModernTsVersion.displayName), }); } else { diff --git a/extensions/typescript-language-features/src/ui/typingsStatus.ts b/extensions/typescript-language-features/src/ui/typingsStatus.ts index 2e0d53b363e..3e8d7c4efac 100644 --- a/extensions/typescript-language-features/src/ui/typingsStatus.ts +++ b/extensions/typescript-language-features/src/ui/typingsStatus.ts @@ -11,7 +11,7 @@ import { Disposable } from '../utils/dispose'; const typingsInstallTimeout = 30 * 1000; export default class TypingsStatus extends Disposable { - private readonly _acquiringTypings = new Map(); + private readonly _acquiringTypings = new Map(); private readonly _client: ITypeScriptServiceClient; constructor(client: ITypeScriptServiceClient) { diff --git a/extensions/typescript-language-features/src/utils/platform.ts b/extensions/typescript-language-features/src/utils/platform.ts index a7bdd8f30ff..ba954f59da3 100644 --- a/extensions/typescript-language-features/src/utils/platform.ts +++ b/extensions/typescript-language-features/src/utils/platform.ts @@ -12,3 +12,8 @@ export function isWeb(): boolean { export function isWebAndHasSharedArrayBuffers(): boolean { return isWeb() && (globalThis as any)['crossOriginIsolated']; } + +export function supportsReadableByteStreams(): boolean { + return isWeb() && 'ReadableByteStreamController' in globalThis; +} + diff --git a/extensions/typescript-language-features/web/src/fileWatcherManager.ts b/extensions/typescript-language-features/web/src/fileWatcherManager.ts index 8c8d7403740..5bbce244688 100644 --- a/extensions/typescript-language-features/web/src/fileWatcherManager.ts +++ b/extensions/typescript-language-features/web/src/fileWatcherManager.ts @@ -40,8 +40,6 @@ export class FileWatcherManager { return FileWatcherManager.noopWatcher; } - console.log('watching file:', path); - this.logger.logVerbose('fs.watchFile', { path }); let uri: URI; diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index bc72fe4cb8b..e43e95500ce 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -95,10 +95,12 @@ resolved "https://registry.yarnpkg.com/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz#aa65abc71eba06749a396598f22263d26f796ac7" integrity sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg== -"@types/node@18.x": - version "18.17.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.11.tgz#c04054659d88bfeba94095f41ef99a8ddf4e1813" - integrity sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/semver@^5.5.0": version "5.5.0" @@ -164,6 +166,11 @@ tas-client@0.2.33: resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.2.33.tgz#451bf114a8a64748030ce4068ab7d079958402e6" integrity sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg== +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== + vscode-tas-client@^0.1.84: version "0.1.84" resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.84.tgz#906bdcfd8c9e1dc04321d6bc0335184f9119968e" diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 25783530305..51a74ad7b00 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -7,8 +7,6 @@ "enabledApiProposals": [ "activeComment", "authSession", - "chatParticipant", - "languageModels", "defaultChatParticipant", "chatVariableResolver", "contribViewsRemote", @@ -243,7 +241,7 @@ }, "devDependencies": { "@types/mocha": "^9.1.1", - "@types/node": "18.x" + "@types/node": "20.x" }, "repository": { "type": "git", diff --git a/extensions/vscode-api-tests/src/memfs.ts b/extensions/vscode-api-tests/src/memfs.ts index b7392ae7195..cd422682499 100644 --- a/extensions/vscode-api-tests/src/memfs.ts +++ b/extensions/vscode-api-tests/src/memfs.ts @@ -218,7 +218,7 @@ export class TestFS implements vscode.FileSystemProvider { private _emitter = new vscode.EventEmitter(); private _bufferedEvents: vscode.FileChangeEvent[] = []; - private _fireSoonHandle?: NodeJS.Timer; + private _fireSoonHandle?: NodeJS.Timeout; readonly onDidChangeFile: vscode.Event = this._emitter.event; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 19de2b62057..ca72f39feb8 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -71,7 +71,7 @@ suite('chat', () => { }); test('participant and variable', async () => { - disposables.push(chat.registerChatVariableResolver('myVar', 'My variable', { + disposables.push(chat.registerChatVariableResolver('myVarId', 'myVar', 'My variable', 'My variable', false, { resolve(_name, _context, _token) { return [{ level: ChatVariableLevel.Full, value: 'myValue' }]; } @@ -81,7 +81,7 @@ suite('chat', () => { commands.executeCommand('workbench.action.chat.open', { query: '@participant hi #myVar' }); const request = await deferred.p; assert.strictEqual(request.prompt, 'hi #myVar'); - assert.strictEqual(request.variables[0].value, 'myValue'); + assert.strictEqual(request.references[0].value, 'myValue'); }); test('result metadata is returned to the followup provider', async () => { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index ee33d61d1e0..cc2f2675297 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { basename } from 'path'; -import { commands, debug, Disposable, window, workspace } from 'vscode'; +import { commands, debug, Disposable, FunctionBreakpoint, window, workspace } from 'vscode'; import { assertNoRpc, createRandomFile, disposeAll } from '../utils'; suite('vscode API - debug', function () { @@ -49,6 +49,17 @@ suite('vscode API - debug', function () { disposeAll(toDispose); }); + test('function breakpoint', async function () { + assert.strictEqual(debug.breakpoints.length, 0); + debug.addBreakpoints([new FunctionBreakpoint('func', false, 'condition', 'hitCondition', 'logMessage')]); + const functionBreakpoint = debug.breakpoints[0] as FunctionBreakpoint; + assert.strictEqual(functionBreakpoint.condition, 'condition'); + assert.strictEqual(functionBreakpoint.hitCondition, 'hitCondition'); + assert.strictEqual(functionBreakpoint.logMessage, 'logMessage'); + assert.strictEqual(functionBreakpoint.enabled, false); + assert.strictEqual(functionBreakpoint.functionName, 'func'); + }); + test('start debugging', async function () { let stoppedEvents = 0; let variablesReceived: () => void; diff --git a/extensions/vscode-api-tests/yarn.lock b/extensions/vscode-api-tests/yarn.lock index 3c35048cdc7..484fa0c5ac5 100644 --- a/extensions/vscode-api-tests/yarn.lock +++ b/extensions/vscode-api-tests/yarn.lock @@ -7,7 +7,14 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index eb72136ccf4..159bd29573b 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -20,7 +20,7 @@ "jsonc-parser": "^3.2.0" }, "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "contributes": { "semanticTokenTypes": [ diff --git a/extensions/vscode-colorize-tests/yarn.lock b/extensions/vscode-colorize-tests/yarn.lock index a7a6fa446ca..88c52293616 100644 --- a/extensions/vscode-colorize-tests/yarn.lock +++ b/extensions/vscode-colorize-tests/yarn.lock @@ -2,12 +2,19 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" jsonc-parser@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + +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== diff --git a/extensions/vscode-test-resolver/package.json b/extensions/vscode-test-resolver/package.json index e538d43f21b..8ab2171ddaa 100644 --- a/extensions/vscode-test-resolver/package.json +++ b/extensions/vscode-test-resolver/package.json @@ -33,7 +33,7 @@ "main": "./out/extension", "browser": "./dist/browser/testResolverMain", "devDependencies": { - "@types/node": "18.x" + "@types/node": "20.x" }, "capabilities": { "untrustedWorkspaces": { diff --git a/extensions/vscode-test-resolver/yarn.lock b/extensions/vscode-test-resolver/yarn.lock index 8a3d10f2b65..1f4b6c2e8b4 100644 --- a/extensions/vscode-test-resolver/yarn.lock +++ b/extensions/vscode-test-resolver/yarn.lock @@ -2,7 +2,14 @@ # yarn lockfile v1 -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" + +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== diff --git a/extensions/yarn.lock b/extensions/yarn.lock index a8cfe5c0351..fa4595ffa74 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -217,10 +217,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-gyp-build@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" - integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-gyp-build@4.8.1, node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== picomatch@^2.3.1: version "2.3.1" diff --git a/package.json b/package.json index 75573b9c564..07f7bae6692 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.90.0", - "distro": "5d328d131fa6f8c6923e6bdec0b6f538318c18e8", + "distro": "0f8d3c619e9a416854972b1f496b1eedc727ab18", "author": { "name": "Microsoft Corporation" }, @@ -70,6 +70,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", + "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", "@vscode/proxy-agent": "^0.19.0", @@ -117,7 +118,7 @@ "@types/kerberos": "^1.1.2", "@types/minimist": "^1.2.1", "@types/mocha": "^9.1.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/sinon": "^10.0.2", "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^1.0.6", @@ -149,7 +150,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "28.2.8", + "electron": "29.4.0", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", @@ -208,7 +209,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.5.0-dev.20240408", + "typescript": "^5.5.0-dev.20240521", "util": "^0.12.4", "vscode-nls-dev": "^3.3.1", "webpack": "^5.91.0", @@ -217,6 +218,9 @@ "xml2js": "^0.5.0", "yaserver": "^0.4.0" }, + "resolutions": { + "node-gyp-build": "4.8.1" + }, "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" diff --git a/remote/.yarnrc b/remote/.yarnrc index 4e7208cdf69..3a01071e2ba 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,5 +1,5 @@ disturl "https://nodejs.org/dist" -target "18.18.2" -ms_build_id "256117" +target "20.9.0" +ms_build_id "274207" runtime "node" build_from_source "true" diff --git a/remote/package.json b/remote/package.json index c0397ce197f..3cdd8501efd 100644 --- a/remote/package.json +++ b/remote/package.json @@ -35,5 +35,8 @@ "vscode-textmate": "9.0.0", "yauzl": "^3.0.0", "yazl": "^2.4.3" + }, + "resolutions": { + "node-gyp-build": "4.8.1" } } diff --git a/remote/yarn.lock b/remote/yarn.lock index 88d688b5954..71e8b408a1f 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -421,10 +421,10 @@ node-addon-api@^4.3.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== -node-gyp-build@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" - integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-gyp-build@4.8.1, node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== node-pty@1.1.0-beta11: version "1.1.0-beta11" diff --git a/resources/server/bin/code-server-linux.sh b/resources/server/bin/code-server-linux.sh index 3df32dfd43c..9229ec89a0e 100644 --- a/resources/server/bin/code-server-linux.sh +++ b/resources/server/bin/code-server-linux.sh @@ -9,4 +9,8 @@ esac ROOT="$(dirname "$(dirname "$(readlink -f "$0")")")" +# workaround for https://github.com/microsoft/vscode/issues/212678 +# Remove this once we update to Node.js >= 20.11.x +export UV_USE_IO_URING=0 + "$ROOT/node" ${INSPECT:-} "$ROOT/out/server-main.js" "$@" diff --git a/src/main.js b/src/main.js index 90de17b278b..9fe5654081d 100644 --- a/src/main.js +++ b/src/main.js @@ -202,7 +202,10 @@ function configureCommandlineSwitchesSync(cliArgs) { 'disable-hardware-acceleration', // override for the color profile to use - 'force-color-profile' + 'force-color-profile', + + // disable LCD font rendering, a Chromium flag + 'disable-lcd-text' ]; if (process.platform === 'linux') { diff --git a/src/tsconfig.json b/src/tsconfig.json index 080fac86bb2..db4e0e67fa2 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -6,6 +6,7 @@ "sourceMap": false, "allowJs": true, "resolveJsonModule": true, + "isolatedModules": true, "outDir": "../out/vs", "types": [ "mocha", diff --git a/src/typings/require.d.ts b/src/typings/require.d.ts index 3fda6d6981d..f051253046f 100644 --- a/src/typings/require.d.ts +++ b/src/typings/require.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -declare const enum LoaderEventType { +declare enum LoaderEventType { LoaderAvailable = 1, BeginLoadingScript = 10, diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index ff113c9baa9..76abbb844ec 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -188,7 +188,7 @@ function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable { let wrapHandler = handler; - if (type === 'click' || type === 'mousedown') { + if (type === 'click' || type === 'mousedown' || type === 'contextmenu') { wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler); } else if (type === 'keydown' || type === 'keypress' || type === 'keyup') { wrapHandler = _wrapAsStandardKeyboardEvent(handler); diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index ea0dde2b599..b1a304845a3 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -17,7 +17,7 @@ import { markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { FileAccess, Schemas } from 'vs/base/common/network'; @@ -36,6 +36,12 @@ export interface MarkdownRenderOptions extends FormattedTextRenderOptions { readonly asyncRenderCallback?: () => void; readonly fillInIncompleteTokens?: boolean; readonly remoteImageIsAllowed?: (uri: URI) => boolean; + readonly sanitizerOptions?: ISanitizerOptions; +} + +export interface ISanitizerOptions { + replaceWithPlaintext?: boolean; + allowedTags?: string[]; } const defaultMarkedRenderers = Object.freeze({ @@ -221,6 +227,10 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende // We always pass the output through dompurify after this so that we don't rely on // marked for sanitization. markedOptions.sanitizer = (html: string): string => { + if (options.sanitizerOptions?.replaceWithPlaintext) { + return escape(html); + } + const match = markdown.isTrusted ? html.match(/^(]+>)|(<\/\s*span>)$/) : undefined; return match ? html : ''; }; @@ -261,7 +271,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende } const htmlParser = new DOMParser(); - const markdownHtmlDoc = htmlParser.parseFromString(sanitizeRenderedMarkdown(markdown, renderedMarkdown) as unknown as string, 'text/html'); + const markdownHtmlDoc = htmlParser.parseFromString(sanitizeRenderedMarkdown({ isTrusted: markdown.isTrusted, ...options.sanitizerOptions }, renderedMarkdown) as unknown as string, 'text/html'); markdownHtmlDoc.body.querySelectorAll('img, audio, video, source') .forEach(img => { @@ -306,7 +316,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende } }); - element.innerHTML = sanitizeRenderedMarkdown(markdown, markdownHtmlDoc.body.innerHTML) as unknown as string; + element.innerHTML = sanitizeRenderedMarkdown({ isTrusted: markdown.isTrusted, ...options.sanitizerOptions }, markdownHtmlDoc.body.innerHTML) as unknown as string; if (codeBlocks.length > 0) { Promise.all(codeBlocks).then((tuples) => { @@ -378,12 +388,19 @@ function resolveWithBaseUri(baseUri: URI, href: string): string { } } +interface IInternalSanitizerOptions extends ISanitizerOptions { + isTrusted?: boolean | MarkdownStringTrustedOptions; +} + +const selfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']; + function sanitizeRenderedMarkdown( - options: { isTrusted?: boolean | MarkdownStringTrustedOptions }, + options: IInternalSanitizerOptions, renderedMarkdown: string, ): TrustedHTML { const { config, allowedSchemes } = getSanitizerOptions(options); - dompurify.addHook('uponSanitizeAttribute', (element, e) => { + const store = new DisposableStore(); + store.add(addDompurifyHook('uponSanitizeAttribute', (element, e) => { if (e.attrName === 'style' || e.attrName === 'class') { if (element.tagName === 'SPAN') { if (e.attrName === 'style') { @@ -403,26 +420,59 @@ function sanitizeRenderedMarkdown( } e.keepAttr = false; } - }); + })); - dompurify.addHook('uponSanitizeElement', (element, e) => { + store.add(addDompurifyHook('uponSanitizeElement', (element, e) => { if (e.tagName === 'input') { if (element.attributes.getNamedItem('type')?.value === 'checkbox') { element.setAttribute('disabled', ''); - } else { + } else if (!options.replaceWithPlaintext) { element.parentElement?.removeChild(element); } } - }); + if (options.replaceWithPlaintext && !e.allowedTags[e.tagName] && e.tagName !== 'body') { + if (element.parentElement) { + let startTagText: string; + let endTagText: string | undefined; + if (e.tagName === '#comment') { + startTagText = ``; + } else { + const isSelfClosing = selfClosingTags.includes(e.tagName); + const attrString = element.attributes.length ? + ' ' + Array.from(element.attributes) + .map(attr => `${attr.name}="${attr.value}"`) + .join(' ') + : ''; + startTagText = `<${e.tagName}${attrString}>`; + if (!isSelfClosing) { + endTagText = ``; + } + } - const hook = DOM.hookDomPurifyHrefAndSrcSanitizer(allowedSchemes); + const fragment = document.createDocumentFragment(); + const textNode = element.parentElement.ownerDocument.createTextNode(startTagText); + fragment.appendChild(textNode); + const endTagTextNode = endTagText ? element.parentElement.ownerDocument.createTextNode(endTagText) : undefined; + while (element.firstChild) { + fragment.appendChild(element.firstChild); + } + + if (endTagTextNode) { + fragment.appendChild(endTagTextNode); + } + + element.parentElement.replaceChild(fragment, element); + } + } + })); + + store.add(DOM.hookDomPurifyHrefAndSrcSanitizer(allowedSchemes)); try { return dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true }); } finally { - dompurify.removeHook('uponSanitizeAttribute'); - hook.dispose(); + store.dispose(); } } @@ -452,7 +502,7 @@ export const allowedMarkdownAttr = [ 'start', ]; -function getSanitizerOptions(options: { readonly isTrusted?: boolean | MarkdownStringTrustedOptions }): { config: dompurify.Config; allowedSchemes: string[] } { +function getSanitizerOptions(options: IInternalSanitizerOptions): { config: dompurify.Config; allowedSchemes: string[] } { const allowedSchemes = [ Schemas.http, Schemas.https, @@ -474,7 +524,7 @@ function getSanitizerOptions(options: { readonly isTrusted?: boolean | MarkdownS // Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure. // HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/ // HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension- - ALLOWED_TAGS: [...DOM.basicMarkupHtmlTags], + ALLOWED_TAGS: options.allowedTags ?? [...DOM.basicMarkupHtmlTags], ALLOWED_ATTR: allowedMarkdownAttr, ALLOW_UNKNOWN_PROTOCOLS: true, }, @@ -492,15 +542,16 @@ export function renderStringAsPlaintext(string: IMarkdownString | string) { /** * Strips all markdown from `markdown`. For example `# Header` would be output as `Header`. + * provide @param withCodeBlocks to retain code blocks */ -export function renderMarkdownAsPlaintext(markdown: IMarkdownString) { +export function renderMarkdownAsPlaintext(markdown: IMarkdownString, withCodeBlocks?: boolean) { // values that are too long will freeze the UI let value = markdown.value ?? ''; if (value.length > 100_000) { value = `${value.substr(0, 100_000)}…`; } - const html = marked.parse(value, { renderer: plainTextRenderer.value }).replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m); + const html = marked.parse(value, { renderer: withCodeBlocks ? plainTextWithCodeBlocksRenderer.value : plainTextRenderer.value }).replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m); return sanitizeRenderedMarkdown({ isTrusted: false }, html).toString(); } @@ -514,7 +565,7 @@ const unescapeInfo = new Map([ ['>', '>'], ]); -const plainTextRenderer = new Lazy(() => { +function createRenderer(): marked.Renderer { const renderer = new marked.Renderer(); renderer.code = (code: string): string => { @@ -578,6 +629,14 @@ const plainTextRenderer = new Lazy(() => { return text; }; return renderer; +} +const plainTextRenderer = new Lazy((withCodeBlocks?: boolean) => createRenderer()); +const plainTextWithCodeBlocksRenderer = new Lazy(() => { + const renderer = createRenderer(); + renderer.code = (code: string): string => { + return '\n' + '```' + code + '```' + '\n'; + }; + return renderer; }); function mergeRawTokenText(tokens: marked.Token[]): string { @@ -707,8 +766,14 @@ function completeListItemPattern(list: marked.Tokens.List): marked.Tokens.List | const previousListItemsText = mergeRawTokenText(list.items.slice(0, -1)); - // Grabbing the `- ` off the list item because I can't find a better way to do this - const newListItemText = lastListItem.raw.slice(0, 2) + + // Grabbing the `- ` or `1. ` off the list item because I can't find a better way to do this + const lastListItemLead = lastListItem.raw.match(/^(\s*(-|\d+\.) +)/)?.[0]; + if (!lastListItemLead) { + // Is badly formatted + return; + } + + const newListItemText = lastListItemLead + mergeRawTokenText(lastListItem.tokens.slice(0, -1)) + newToken.raw; @@ -867,3 +932,16 @@ function completeTable(tokens: marked.Token[]): marked.Token[] | undefined { return undefined; } + +function addDompurifyHook( + hook: 'uponSanitizeElement', + cb: (currentNode: Element, data: dompurify.SanitizeElementHookEvent, config: dompurify.Config) => void, +): IDisposable; +function addDompurifyHook( + hook: 'uponSanitizeAttribute', + cb: (currentNode: Element, data: dompurify.SanitizeAttributeHookEvent, config: dompurify.Config) => void, +): IDisposable; +function addDompurifyHook(hook: 'uponSanitizeElement' | 'uponSanitizeAttribute', cb: any): IDisposable { + dompurify.addHook(hook, cb); + return toDisposable(() => dompurify.removeHook(hook)); +} diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 5491cd6590e..3c42632c500 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -24,6 +24,7 @@ import 'vs/css!./button'; import { localize } from 'vs/nls'; import type { IUpdatableHover } from 'vs/base/browser/ui/hover/hover'; import { getBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; +import { IActionProvider } from 'vs/base/browser/ui/dropdown/dropdown'; export interface IButtonOptions extends Partial { readonly title?: boolean | string; @@ -303,7 +304,7 @@ export class Button extends Disposable implements IButton { return !this._element.classList.contains('disabled'); } - private setTitle(title: string) { + setTitle(title: string) { if (!this._hover && title !== '') { this._hover = this._register(getBaseLayerHoverDelegate().setupUpdatableHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this._element, title)); } else if (this._hover) { @@ -322,7 +323,7 @@ export class Button extends Disposable implements IButton { export interface IButtonWithDropdownOptions extends IButtonOptions { readonly contextMenuProvider: IContextMenuProvider; - readonly actions: readonly IAction[]; + readonly actions: readonly IAction[] | IActionProvider; readonly actionRunner?: IActionRunner; readonly addPrimaryActionToDropdown?: boolean; } @@ -375,9 +376,10 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.dropdownButton.element.classList.add('monaco-dropdown-button'); this.dropdownButton.icon = Codicon.dropDownButton; this._register(this.dropdownButton.onDidClick(e => { + const actions = Array.isArray(options.actions) ? options.actions : (options.actions as IActionProvider).getActions(); options.contextMenuProvider.showContextMenu({ getAnchor: () => this.dropdownButton.element, - getActions: () => options.addPrimaryActionToDropdown === false ? [...options.actions] : [this.action, ...options.actions], + getActions: () => options.addPrimaryActionToDropdown === false ? [...actions] : [this.action, ...actions], actionRunner: options.actionRunner, onHide: () => this.dropdownButton.element.setAttribute('aria-expanded', 'false') }); diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 807c949a277..27ee4c68cae 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/hover/hover.ts b/src/vs/base/browser/ui/hover/hover.ts index a0f9422ce05..f2b7582d7fa 100644 --- a/src/vs/base/browser/ui/hover/hover.ts +++ b/src/vs/base/browser/ui/hover/hover.ts @@ -43,6 +43,11 @@ export interface IHoverDelegate2 { // TODO: Change hoverDelegate arg to exclude the actual delegate and instead use the new options setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions): IUpdatableHover; + + /** + * Shows the hover for the given element if one has been setup. + */ + triggerUpdatableHover(htmlElement: HTMLElement): void; } export interface IHoverWidget extends IDisposable { @@ -246,6 +251,7 @@ export type IUpdatableHoverContentOrFactory = IUpdatableHoverContent | (() => IU export interface IUpdatableHoverOptions { actions?: IHoverAction[]; linkHandler?(url: string): void; + trapFocus?: boolean; } export interface IUpdatableHover extends IDisposable { diff --git a/src/vs/base/browser/ui/hover/hoverDelegate2.ts b/src/vs/base/browser/ui/hover/hoverDelegate2.ts index 90c71d65a1d..13a379222c1 100644 --- a/src/vs/base/browser/ui/hover/hoverDelegate2.ts +++ b/src/vs/base/browser/ui/hover/hoverDelegate2.ts @@ -10,6 +10,7 @@ let baseHoverDelegate: IHoverDelegate2 = { hideHover: () => undefined, showAndFocusLastHover: () => undefined, setupUpdatableHover: () => null!, + triggerUpdatableHover: () => undefined }; /** diff --git a/src/vs/base/browser/ui/selectBox/selectBox.ts b/src/vs/base/browser/ui/selectBox/selectBox.ts index 629a39ea1d2..d5329939ca2 100644 --- a/src/vs/base/browser/ui/selectBox/selectBox.ts +++ b/src/vs/base/browser/ui/selectBox/selectBox.ts @@ -28,6 +28,7 @@ export interface ISelectBoxDelegate extends IDisposable { focus(): void; blur(): void; setFocusable(focus: boolean): void; + setEnabled(enabled: boolean): void; // Delegated Widget interface render(container: HTMLElement): void; @@ -124,6 +125,10 @@ export class SelectBox extends Widget implements ISelectBoxDelegate { this.selectBoxDelegate.setFocusable(focusable); } + setEnabled(enabled: boolean): void { + this.selectBoxDelegate.setEnabled(enabled); + } + render(container: HTMLElement): void { this.selectBoxDelegate.render(container); } diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 0506315a7ae..a58782d95df 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -299,6 +299,9 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } } + public setEnabled(enable: boolean): void { + this.selectElement.disabled = !enable; + } private setOptionsList() { diff --git a/src/vs/base/browser/ui/selectBox/selectBoxNative.ts b/src/vs/base/browser/ui/selectBox/selectBoxNative.ts index 12cec70bdf9..896ac0e4bd0 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxNative.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxNative.ts @@ -148,6 +148,10 @@ export class SelectBoxNative extends Disposable implements ISelectBoxDelegate { } } + public setEnabled(enable: boolean): void { + this.selectElement.disabled = !enable; + } + public setFocusable(focusable: boolean): void { this.selectElement.tabIndex = focusable ? 0 : -1; } diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index d7d01624199..a52c00287d2 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -235,6 +235,8 @@ export class Toggle extends Widget { export class Checkbox extends Widget { + static readonly CLASS_NAME = 'monaco-checkbox'; + private readonly _onChange = this._register(new Emitter()); readonly onChange: Event = this._onChange.event; @@ -246,7 +248,7 @@ export class Checkbox extends Widget { constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) { super(); - this.checkbox = this._register(new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles })); + this.checkbox = this._register(new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: Checkbox.CLASS_NAME, ...unthemedToggleStyles })); this.domNode = this.checkbox.domNode; diff --git a/src/vs/base/common/assert.ts b/src/vs/base/common/assert.ts index 4ded48fb1de..bbd344d55c7 100644 --- a/src/vs/base/common/assert.ts +++ b/src/vs/base/common/assert.ts @@ -29,9 +29,9 @@ export function assertNever(value: never, message = 'Unreachable'): never { throw new Error(message); } -export function assert(condition: boolean): void { +export function assert(condition: boolean, message = 'unexpected state'): asserts condition { if (!condition) { - throw new BugIndicatingError('Assertion Failed'); + throw new BugIndicatingError(`Assertion Failed: ${message}`); } } diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 02e911ebd32..bf6342078da 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -880,6 +880,7 @@ export class ResourceQueue implements IDisposable { export class TimeoutTimer implements IDisposable { private _token: any; + private _isDisposed = false; constructor(); constructor(runner: () => void, timeout: number); @@ -893,6 +894,7 @@ export class TimeoutTimer implements IDisposable { dispose(): void { this.cancel(); + this._isDisposed = true; } cancel(): void { @@ -903,6 +905,10 @@ export class TimeoutTimer implements IDisposable { } cancelAndSet(runner: () => void, timeout: number): void { + if (this._isDisposed) { + throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed TimeoutTimer`); + } + this.cancel(); this._token = setTimeout(() => { this._token = -1; @@ -911,6 +917,10 @@ export class TimeoutTimer implements IDisposable { } setIfNotSet(runner: () => void, timeout: number): void { + if (this._isDisposed) { + throw new BugIndicatingError(`Calling 'setIfNotSet' on a disposed TimeoutTimer`); + } + if (this._token !== -1) { // timer is already set return; @@ -925,6 +935,7 @@ export class TimeoutTimer implements IDisposable { export class IntervalTimer implements IDisposable { private disposable: IDisposable | undefined = undefined; + private isDisposed = false; cancel(): void { this.disposable?.dispose(); @@ -932,6 +943,10 @@ export class IntervalTimer implements IDisposable { } cancelAndSet(runner: () => void, interval: number, context = globalThis): void { + if (this.isDisposed) { + throw new BugIndicatingError(`Calling 'cancelAndSet' on a disposed IntervalTimer`); + } + this.cancel(); const handle = context.setInterval(() => { runner(); @@ -945,6 +960,7 @@ export class IntervalTimer implements IDisposable { dispose(): void { this.cancel(); + this.isDisposed = true; } } diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 03118140340..599873fbacf 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -578,4 +578,5 @@ export const codiconsLibrary = { goToSearch: register('go-to-search', 0xec32), percentage: register('percentage', 0xec33), sortPercentage: register('sort-percentage', 0xec33), + attach: register('attach', 0xec34), } as const; diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index e468ba9b780..f94fa3673b6 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -835,6 +835,7 @@ class LeakageMonitor { private _warnCountdown: number = 0; constructor( + private readonly _errorHandler: (err: Error) => void, readonly threshold: number, readonly name: string = Math.random().toString(18).slice(2, 5), ) { } @@ -862,18 +863,13 @@ class LeakageMonitor { // is exceeded by 50% again this._warnCountdown = threshold * 0.5; - // find most frequent listener and print warning - let topStack: string | undefined; - let topCount: number = 0; - for (const [stack, count] of this._stacks) { - if (!topStack || topCount < count) { - topStack = stack; - topCount = count; - } - } - - console.warn(`[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`); + const [topStack, topCount] = this.getMostFrequentStack()!; + const message = `[${this.name}] potential listener LEAK detected, having ${listenerCount} listeners already. MOST frequent listener (${topCount}):`; + console.warn(message); console.warn(topStack!); + + const error = new ListenerLeakError(message, topStack); + this._errorHandler(error); } return () => { @@ -881,12 +877,28 @@ class LeakageMonitor { this._stacks!.set(stack.value, count - 1); }; } + + getMostFrequentStack(): [string, number] | undefined { + if (!this._stacks) { + return undefined; + } + let topStack: [string, number] | undefined; + let topCount: number = 0; + for (const [stack, count] of this._stacks) { + if (!topStack || topCount < count) { + topStack = [stack, count]; + topCount = count; + } + } + return topStack; + } } class Stacktrace { static create() { - return new Stacktrace(new Error().stack ?? ''); + const err = new Error(); + return new Stacktrace(err.stack ?? ''); } private constructor(readonly value: string) { } @@ -896,6 +908,25 @@ class Stacktrace { } } +// error that is logged when going over the configured listener threshold +export class ListenerLeakError extends Error { + constructor(message: string, stack: string) { + super(message); + this.name = 'ListenerLeakError'; + this.stack = stack; + } +} + +// SEVERE error that is logged when having gone way over the configured listener +// threshold so that the emitter refuses to accept more listeners +export class ListenerRefusalError extends Error { + constructor(message: string, stack: string) { + super(message); + this.name = 'ListenerRefusalError'; + this.stack = stack; + } +} + let id = 0; class UniqueContainer { stack?: Stacktrace; @@ -988,7 +1019,9 @@ export class Emitter { constructor(options?: EmitterOptions) { this._options = options; - this._leakageMon = _globalLeakWarningThreshold > 0 || this._options?.leakWarningThreshold ? new LeakageMonitor(this._options?.leakWarningThreshold ?? _globalLeakWarningThreshold) : undefined; + this._leakageMon = (_globalLeakWarningThreshold > 0 || this._options?.leakWarningThreshold) + ? new LeakageMonitor(options?.onListenerError ?? onUnexpectedError, this._options?.leakWarningThreshold ?? _globalLeakWarningThreshold) : + undefined; this._perfMon = this._options?._profName ? new EventProfiling(this._options._profName) : undefined; this._deliveryQueue = this._options?.deliveryQueue as EventDeliveryQueuePrivate | undefined; } @@ -1032,8 +1065,15 @@ export class Emitter { */ get event(): Event { this._event ??= (callback: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => { - if (this._leakageMon && this._size > this._leakageMon.threshold * 3) { - console.warn(`[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far`); + if (this._leakageMon && this._size > this._leakageMon.threshold ** 2) { + const message = `[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size} vs ${this._leakageMon.threshold})`; + console.warn(message); + + const tuple = this._leakageMon.getMostFrequentStack() ?? ['UNKNOWN stack', -1]; + const error = new ListenerRefusalError(`${message}. HINT: Stack shows most frequent listener (${tuple[1]}-times)`, tuple[0]); + const errorHandler = this._options?.onListenerError || onUnexpectedError; + errorHandler(error); + return Disposable.None; } diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index d7f6723092c..e0ee6968dce 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -164,7 +164,7 @@ export function isUNC(path: string): boolean { // Reference: https://en.wikipedia.org/wiki/Filename const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g; -const UNIX_INVALID_FILE_CHARS = /[\\/]/g; +const UNIX_INVALID_FILE_CHARS = /[/]/g; const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])(\.(.*?))?$/i; export function isValidBasename(name: string | null | undefined, isWindowsOS: boolean = isWindows): boolean { const invalidFileChars = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS; diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index fcb5d2ec4e4..568a0124c12 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -777,6 +777,16 @@ export class DisposableMap implements ID this._store.delete(key); } + /** + * Delete the value stored for `key` from this map but return it. The caller is + * responsible for disposing of the value. + */ + deleteAndLeak(key: K): V | undefined { + const value = this._store.get(key); + this._store.delete(key); + return value; + } + keys(): IterableIterator { return this._store.keys(); } diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index a128a42b722..14ec0e71974 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -264,3 +264,11 @@ export function createProxyObject(methodNames: string[], invok } return result; } + +export function mapValues(obj: T, fn: (value: T[keyof T], key: string) => R): { [K in keyof T]: R } { + const result: { [key: string]: R } = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = fn(value, key); + } + return result as { [K in keyof T]: R }; +} diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index 6c14cb20c5b..a2f169ee4d6 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -253,7 +253,7 @@ export class AutorunObserver implements IObserver, IReader const shouldReact = this._handleChange ? this._handleChange({ changedObservable: observable, change, - didChange: o => o === observable as any, + didChange: (o): this is any => o === observable as any, }, this.changeSummary!) : true; if (shouldReact) { this.state = AutorunState.stale; diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 4111141589c..9e95bf9dccc 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -350,7 +350,7 @@ export class Derived extends BaseObservable im const shouldReact = this._handleChange ? this._handleChange({ changedObservable: observable, change, - didChange: o => o === observable as any, + didChange: (o): this is any => o === observable as any, }, this.changeSummary!) : true; const wasUpToDate = this.state === DerivedState.upToDate; if (shouldReact && (this.state === DerivedState.dependenciesMightHaveChanged || wasUpToDate)) { diff --git a/src/vs/base/common/prefixTree.ts b/src/vs/base/common/prefixTree.ts index 0c2bdca384b..8e839e2b4ff 100644 --- a/src/vs/base/common/prefixTree.ts +++ b/src/vs/base/common/prefixTree.ts @@ -46,6 +46,11 @@ export class WellDefinedPrefixTree { this.opNode(key, n => n._value = mutate(n._value === unset ? undefined : n._value)); } + /** Mutates nodes along the path in the prefix tree. */ + mutatePath(key: Iterable, mutate: (node: IPrefixTreeNode) => void): void { + this.opNode(key, () => { }, n => mutate(n)); + } + /** Deletes a node from the prefix tree, returning the value it contained. */ delete(key: Iterable): V | undefined { const path = this.getPathToKey(key); diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 79d22ee0e6d..70bded5a67d 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -1249,6 +1249,16 @@ export class AmbiguousCharacters { return this.confusableDictionary.has(codePoint); } + public containsAmbiguousCharacter(str: string): boolean { + for (let i = 0; i < str.length; i++) { + const codePoint = str.codePointAt(i); + if (typeof codePoint === 'number' && this.isAmbiguous(codePoint)) { + return true; + } + } + return false; + } + /** * Returns the non basic ASCII code point that the given code point can be confused, * or undefined if such code point does note exist. @@ -1281,6 +1291,17 @@ export class InvisibleCharacters { return InvisibleCharacters.getData().has(codePoint); } + public static containsInvisibleCharacter(str: string): boolean { + for (let i = 0; i < str.length; i++) { + const codePoint = str.codePointAt(i); + if (typeof codePoint === 'number' && InvisibleCharacters.isInvisibleCharacter(codePoint)) { + return true; + } + } + return false; + + } + public static get codePoints(): ReadonlySet { return InvisibleCharacters.getData(); } diff --git a/src/vs/base/node/id.ts b/src/vs/base/node/id.ts index 42a7f771358..043731f666d 100644 --- a/src/vs/base/node/id.ts +++ b/src/vs/base/node/id.ts @@ -114,3 +114,14 @@ export async function getSqmMachineId(errorLogger: (error: any) => void): Promis } return ''; } + +export async function getdevDeviceId(errorLogger: (error: any) => void): Promise { + try { + const deviceIdPackage = await import('@vscode/deviceid'); + const id = await deviceIdPackage.getDeviceId(); + return id; + } catch (err) { + errorLogger(err); + return ''; + } +} diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index a71a7b8cb4e..9c07f106711 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -11,7 +11,7 @@ import * as process from 'vs/base/common/process'; import { CommandOptions, ForkOptions, Source, SuccessData, TerminateResponse, TerminateResponseCode } from 'vs/base/common/processes'; import * as Types from 'vs/base/common/types'; import * as pfs from 'vs/base/node/pfs'; -export { CommandOptions, ForkOptions, SuccessData, Source, TerminateResponse, TerminateResponseCode }; +export { type CommandOptions, type ForkOptions, type SuccessData, Source, type TerminateResponse, TerminateResponseCode }; export type ValueCallback = (value: T | Promise) => void; export type ErrorCallback = (error?: any) => void; diff --git a/src/vs/base/node/shell.ts b/src/vs/base/node/shell.ts index 9da701ffec3..55d97ca3580 100644 --- a/src/vs/base/node/shell.ts +++ b/src/vs/base/node/shell.ts @@ -33,7 +33,7 @@ function getSystemShellUnixLike(os: platform.OperatingSystem, env: platform.IPro } if (!_TERMINAL_DEFAULT_SHELL_UNIX_LIKE) { - let unixLikeTerminal: string | undefined; + let unixLikeTerminal: string | undefined | null; if (platform.isWindows) { unixLikeTerminal = '/bin/bash'; // for WSL } else { diff --git a/src/vs/base/parts/sandbox/common/electronTypes.ts b/src/vs/base/parts/sandbox/common/electronTypes.ts index f8c7a35e077..43fa75079d4 100644 --- a/src/vs/base/parts/sandbox/common/electronTypes.ts +++ b/src/vs/base/parts/sandbox/common/electronTypes.ts @@ -7,7 +7,7 @@ // ####################################################################### // ### ### // ### electron.d.ts types we need in a common layer for reuse ### -// ### (copied from Electron 16.x) ### +// ### (copied from Electron 29.x) ### // ### ### // ####################################################################### @@ -148,9 +148,9 @@ export interface SaveDialogReturnValue { */ canceled: boolean; /** - * If the dialog is canceled, this will be `undefined`. + * If the dialog is canceled, this will be an empty string. */ - filePath?: string; + filePath: string; /** * Base64 encoded string which contains the security scoped bookmark data for the * saved file. `securityScopedBookmarks` must be enabled for this to be present. @@ -219,16 +219,20 @@ export interface FileFilter { export interface OpenDevToolsOptions { /** - * Opens the devtools with specified dock state, can be `right`, `bottom`, + * Opens the devtools with specified dock state, can be `left`, `right`, `bottom`, * `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's * possible to dock back. In `detach` mode it's not. */ - mode: ('right' | 'bottom' | 'undocked' | 'detach'); + mode: ('left' | 'right' | 'bottom' | 'undocked' | 'detach'); /** * Whether to bring the opened devtools window to the foreground. The default is * `true`. */ activate?: boolean; + /** + * A title for the DevTools window (only in `undocked` or `detach` mode). + */ + title?: string; } interface InputEvent { @@ -241,6 +245,19 @@ interface InputEvent { * `middleButtonDown`, `rightButtonDown`, `capsLock`, `numLock`, `left`, `right`. */ modifiers?: Array<'shift' | 'control' | 'ctrl' | 'alt' | 'meta' | 'command' | 'cmd' | 'isKeypad' | 'isAutoRepeat' | 'leftButtonDown' | 'middleButtonDown' | 'rightButtonDown' | 'capsLock' | 'numLock' | 'left' | 'right'>; + /** + * Can be `undefined`, `mouseDown`, `mouseUp`, `mouseMove`, `mouseEnter`, + * `mouseLeave`, `contextMenu`, `mouseWheel`, `rawKeyDown`, `keyDown`, `keyUp`, + * `char`, `gestureScrollBegin`, `gestureScrollEnd`, `gestureScrollUpdate`, + * `gestureFlingStart`, `gestureFlingCancel`, `gesturePinchBegin`, + * `gesturePinchEnd`, `gesturePinchUpdate`, `gestureTapDown`, `gestureShowPress`, + * `gestureTap`, `gestureTapCancel`, `gestureShortPress`, `gestureLongPress`, + * `gestureLongTap`, `gestureTwoFingerTap`, `gestureTapUnconfirmed`, + * `gestureDoubleTap`, `touchStart`, `touchMove`, `touchEnd`, `touchCancel`, + * `touchScrollStarted`, `pointerDown`, `pointerUp`, `pointerMove`, + * `pointerRawUpdate`, `pointerCancel` or `pointerCausedUaAction`. + */ + type: ('undefined' | 'mouseDown' | 'mouseUp' | 'mouseMove' | 'mouseEnter' | 'mouseLeave' | 'contextMenu' | 'mouseWheel' | 'rawKeyDown' | 'keyDown' | 'keyUp' | 'char' | 'gestureScrollBegin' | 'gestureScrollEnd' | 'gestureScrollUpdate' | 'gestureFlingStart' | 'gestureFlingCancel' | 'gesturePinchBegin' | 'gesturePinchEnd' | 'gesturePinchUpdate' | 'gestureTapDown' | 'gestureShowPress' | 'gestureTap' | 'gestureTapCancel' | 'gestureShortPress' | 'gestureLongPress' | 'gestureLongTap' | 'gestureTwoFingerTap' | 'gestureTapUnconfirmed' | 'gestureDoubleTap' | 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel' | 'touchScrollStarted' | 'pointerDown' | 'pointerUp' | 'pointerMove' | 'pointerRawUpdate' | 'pointerCancel' | 'pointerCausedUaAction'); } export interface MouseInputEvent extends InputEvent { diff --git a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts index d32188d1e97..ba8ea6446a6 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts @@ -7,7 +7,7 @@ // ####################################################################### // ### ### // ### electron.d.ts types we expose from electron-sandbox ### -// ### (copied from Electron 25.x) ### +// ### (copied from Electron 29.x) ### // ### ### // ####################################################################### @@ -30,20 +30,6 @@ export interface IpcRendererEvent extends Event { * The `IpcRenderer` instance that emitted the event originally */ sender: IpcRenderer; - /** - * The `webContents.id` that sent the message, you can call - * `event.sender.sendTo(event.senderId, ...)` to reply to the message, see - * ipcRenderer.sendTo for more information. This only applies to messages sent from - * a different renderer. Messages sent directly from the main process set - * `event.senderId` to `0`. - */ - senderId: number; - /** - * Whether the message sent via ipcRenderer.sendTo was sent by the main frame. This - * is relevant when `nodeIntegrationInSubFrames` is enabled in the originating - * `webContents`. - */ - senderIsMainFrame?: boolean; } export interface IpcRenderer { @@ -91,10 +77,6 @@ export interface IpcRenderer { * only the next time a message is sent to `channel`, after which it is removed. */ once(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; - /** - * Removes the specified `listener` from the listener array for the specified - * `channel`. - */ // Note: API with `Transferable` intentionally commented out because you // cannot transfer these when `contextIsolation: true`. // /** @@ -111,7 +93,11 @@ export interface IpcRenderer { // * documentation. // */ // postMessage(channel: string, message: any, transfer?: MessagePort[]): void; - removeListener(channel: string, listener: (...args: any[]) => void): this; + /** + * Removes the specified `listener` from the listener array for the specified + * `channel`. + */ + removeListener(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; /** * Send an asynchronous message to the main process via `channel`, along with * arguments. Arguments will be serialized with the Structured Clone Algorithm, diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 44a54904e26..9a01b61c669 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -12,6 +12,11 @@ import { IpcRenderer, ProcessMemoryInfo, WebFrame } from 'vs/base/parts/sandbox/ */ export interface ISandboxNodeProcess extends INodeProcess { + /** + * The process.pid property returns the process ID of the process. + */ + readonly pid: number; + /** * The process.platform property returns a string identifying the operating system platform * on which the Node.js process is running. diff --git a/src/vs/base/parts/sandbox/electron-sandbox/preload.js b/src/vs/base/parts/sandbox/electron-sandbox/preload.js index 90ac940861f..4c51b45d18b 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/preload.js +++ b/src/vs/base/parts/sandbox/electron-sandbox/preload.js @@ -248,6 +248,7 @@ * @type {ISandboxNodeProcess} */ process: { + get pid() { return process.pid; }, get platform() { return process.platform; }, get arch() { return process.arch; }, get env() { return { ...process.env }; }, diff --git a/src/vs/base/parts/sandbox/node/electronTypes.ts b/src/vs/base/parts/sandbox/node/electronTypes.ts index 3d108454bc4..37629888c19 100644 --- a/src/vs/base/parts/sandbox/node/electronTypes.ts +++ b/src/vs/base/parts/sandbox/node/electronTypes.ts @@ -11,6 +11,7 @@ export interface MessagePortMain extends NodeJS.EventEmitter { * Emitted when the remote end of a MessagePortMain object becomes disconnected. */ on(event: 'close', listener: Function): this; + off(event: 'close', listener: Function): this; once(event: 'close', listener: Function): this; addListener(event: 'close', listener: Function): this; removeListener(event: 'close', listener: Function): this; @@ -18,6 +19,7 @@ export interface MessagePortMain extends NodeJS.EventEmitter { * Emitted when a MessagePortMain object receives a message. */ on(event: 'message', listener: (messageEvent: MessageEvent) => void): this; + off(event: 'message', listener: (messageEvent: MessageEvent) => void): this; once(event: 'message', listener: (messageEvent: MessageEvent) => void): this; addListener(event: 'message', listener: (messageEvent: MessageEvent) => void): this; removeListener(event: 'message', listener: (messageEvent: MessageEvent) => void): this; @@ -51,6 +53,7 @@ export interface ParentPort extends NodeJS.EventEmitter { * be queued up until a handler is registered for this event. */ on(event: 'message', listener: (messageEvent: MessageEvent) => void): this; + off(event: 'message', listener: (messageEvent: MessageEvent) => void): this; once(event: 'message', listener: (messageEvent: MessageEvent) => void): this; addListener(event: 'message', listener: (messageEvent: MessageEvent) => void): this; removeListener(event: 'message', listener: (messageEvent: MessageEvent) => void): this; diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index 3a42aba4374..f9099c653e9 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -606,6 +606,15 @@ const y = 2; const completeTokens = marked.lexer(text + delimiter); assert.deepStrictEqual(newTokens, completeTokens); }); + + test(`incomplete ${name} in numbered list`, () => { + const text = `1. list item one\n2. list item two and ${delimiter}text`; + const tokens = marked.lexer(text); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(text + delimiter); + assert.deepStrictEqual(newTokens, completeTokens); + }); } suite('list', () => { @@ -646,6 +655,18 @@ const y = 2; assert.deepStrictEqual(newTokens, tokens); }); + test('ordered list with subitems', () => { + const list = `1. hello + - sub item +2. text + newline for some reason +`; + const tokens = marked.lexer(list); + const newTokens = fillInIncompleteTokens(tokens); + + assert.deepStrictEqual(newTokens, tokens); + }); + test('list with stuff', () => { const list = `- list item one \`codespan\` **bold** [link](http://microsoft.com) more text`; const tokens = marked.lexer(list); @@ -674,6 +695,36 @@ const y = 2; assert.deepStrictEqual(newTokens, completeTokens); }); + test('ordered list with incomplete link target', () => { + const incomplete = `1. list item one +2. item two [link](`; + const tokens = marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(incomplete + ')'); + assert.deepStrictEqual(newTokens, completeTokens); + }); + + test('ordered list with extra whitespace', () => { + const incomplete = `1. list item one +2. item two [link](`; + const tokens = marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(incomplete + ')'); + assert.deepStrictEqual(newTokens, completeTokens); + }); + + test('list with extra whitespace', () => { + const incomplete = `- list item one +- item two [link](`; + const tokens = marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(incomplete + ')'); + assert.deepStrictEqual(newTokens, completeTokens); + }); + test('list with incomplete link with other stuff', () => { const incomplete = `- list item one - item two [\`link`; @@ -683,6 +734,16 @@ const y = 2; const completeTokens = marked.lexer(incomplete + '\`](https://microsoft.com)'); assert.deepStrictEqual(newTokens, completeTokens); }); + + test('ordered list with incomplete link with other stuff', () => { + const incomplete = `1. list item one +1. item two [\`link`; + const tokens = marked.lexer(incomplete); + const newTokens = fillInIncompleteTokens(tokens); + + const completeTokens = marked.lexer(incomplete + '\`](https://microsoft.com)'); + assert.deepStrictEqual(newTokens, completeTokens); + }); }); suite('codespan', () => { diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 49962c89d5a..161c7085fa6 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { stub } from 'sinon'; +import { tail2 } from 'vs/base/common/arrays'; import { DeferredPromise, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors'; -import { AsyncEmitter, DebounceEmitter, DynamicListEventMultiplexer, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, MicrotaskEmitter, PauseableEmitter, Relay, createEventDeliveryQueue } from 'vs/base/common/event'; +import { AsyncEmitter, DebounceEmitter, DynamicListEventMultiplexer, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, ListenerLeakError, ListenerRefusalError, MicrotaskEmitter, PauseableEmitter, Relay, createEventDeliveryQueue } from 'vs/base/common/event'; import { DisposableStore, IDisposable, isDisposable, setDisposableTracker, DisposableTracker } from 'vs/base/common/lifecycle'; import { observableValue, transaction } from 'vs/base/common/observable'; import { MicrotaskDelay } from 'vs/base/common/symbols'; @@ -368,6 +369,31 @@ suite('Event', function () { }); + test('throw ListenerLeakError', () => { + + const store = new DisposableStore(); + const allError: any[] = []; + + const a = ds.add(new Emitter({ + onListenerError(e) { allError.push(e); }, + leakWarningThreshold: 3, + })); + + for (let i = 0; i < 11; i++) { + a.event(() => { }, undefined, store); + } + + assert.deepStrictEqual(allError.length, 5); + const [start, tail] = tail2(allError); + assert.ok(tail instanceof ListenerRefusalError); + + for (const item of start) { + assert.ok(item instanceof ListenerLeakError); + } + + store.dispose(); + }); + test('reusing event function and context', function () { let counter = 0; function listener() { diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index 3c6a4e4979b..c13210daa13 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -50,9 +50,9 @@ suite('Paths', () => { assert.ok(!extpath.isValidBasename('')); assert.ok(extpath.isValidBasename('test.txt')); assert.ok(!extpath.isValidBasename('/test.txt')); - assert.ok(!extpath.isValidBasename('\\test.txt')); if (isWindows) { + assert.ok(!extpath.isValidBasename('\\test.txt')); assert.ok(!extpath.isValidBasename('aux')); assert.ok(!extpath.isValidBasename('Aux')); assert.ok(!extpath.isValidBasename('LPT0')); @@ -69,6 +69,8 @@ suite('Paths', () => { assert.ok(!extpath.isValidBasename('test.txt\t')); assert.ok(!extpath.isValidBasename('tes:t.txt')); assert.ok(!extpath.isValidBasename('tes"t.txt')); + } else { + assert.ok(extpath.isValidBasename('\\test.txt')); } }); diff --git a/src/vs/base/test/common/objects.test.ts b/src/vs/base/test/common/objects.test.ts index b3d0940f8fa..2465585544f 100644 --- a/src/vs/base/test/common/objects.test.ts +++ b/src/vs/base/test/common/objects.test.ts @@ -233,3 +233,19 @@ suite('Objects', () => { assert.strictEqual(obj1.mIxEdCaSe, objects.getCaseInsensitive(obj1, 'mixedcase')); }); }); + +test('mapValues', () => { + const obj = { + a: 1, + b: 2, + c: 3 + }; + + const result = objects.mapValues(obj, (value, key) => `${key}: ${value * 2}`); + + assert.deepStrictEqual(result, { + a: 'a: 2', + b: 'b: 4', + c: 'c: 6', + }); +}); diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index bb9e5db1d51..4be439f4656 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -558,6 +558,22 @@ suite('Strings', () => { assert.strictEqual(strings.count('hello world', 'foo'), 0); }); + test('containsAmbiguousCharacter', () => { + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('abcd'), false); + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('üå'), false); + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('(*&^)'), false); + + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('ο'), true); + assert.strictEqual(strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter('abɡc'), true); + }); + + test('containsInvisibleCharacter', () => { + assert.strictEqual(strings.InvisibleCharacters.containsInvisibleCharacter('abcd'), false); + assert.strictEqual(strings.InvisibleCharacters.containsInvisibleCharacter(' '), true); + assert.strictEqual(strings.InvisibleCharacters.containsInvisibleCharacter('a\u{e004e}b'), true); + assert.strictEqual(strings.InvisibleCharacters.containsInvisibleCharacter('a\u{e015a}\u000bb'), true); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/node/id.test.ts b/src/vs/base/test/node/id.test.ts index 47580ff957c..1a629134f06 100644 --- a/src/vs/base/test/node/id.test.ts +++ b/src/vs/base/test/node/id.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { getMachineId, getSqmMachineId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { getMac } from 'vs/base/node/macAddress'; import { flakySuite } from 'vs/base/test/node/testUtils'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -26,6 +26,13 @@ flakySuite('ID', () => { assert.strictEqual(errors.length, 0); }); + test('getdevDeviceId', async function () { + const errors = []; + const id = await getdevDeviceId(err => errors.push(err)); + assert.ok(typeof id === 'string'); + assert.strictEqual(errors.length, 0); + }); + test('getMac', async () => { const macAddress = getMac(); assert.ok(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(macAddress), `Expected a MAC address, got: ${macAddress}`); diff --git a/src/vs/code/browser/issue/issue.ts b/src/vs/code/browser/issue/issue.ts new file mode 100644 index 00000000000..2205e847134 --- /dev/null +++ b/src/vs/code/browser/issue/issue.ts @@ -0,0 +1,1197 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IProductConfiguration } from 'vs/base/common/product'; +import { $, createStyleSheet, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { Button, unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Delayer, RunOnceScheduler } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; +import { debounce } from 'vs/base/common/decorators'; +import { CancellationError } from 'vs/base/common/errors'; +import { isLinuxSnap } from 'vs/base/common/platform'; +import { escape } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/browser/issue/issueReporterModel'; +import { localize } from 'vs/nls'; +import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; +import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; +import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet'; + +const MAX_URL_LENGTH = 7500; + +interface SearchResult { + html_url: string; + title: string; + state?: string; +} + +enum IssueSource { + VSCode = 'vscode', + Extension = 'extension', + Marketplace = 'marketplace', + Unknown = 'unknown' +} + + +export class BaseIssueReporterService extends Disposable { + public issueReporterModel: IssueReporterModel; + public receivedSystemInfo = false; + public numberOfSearchResultsDisplayed = 0; + public receivedPerformanceInfo = false; + public shouldQueueSearch = false; + public hasBeenSubmitted = false; + public openReporter = false; + public loadingExtensionData = false; + public selectedExtension = ''; + public delayedSubmit = new Delayer(300); + public previewButton!: Button; + public nonGitHubIssueUrl = false; + + constructor( + public disableExtensions: boolean, + public data: IssueReporterData, + public os: { + type: string; + arch: string; + release: string; + }, + public product: IProductConfiguration, + public readonly window: Window, + public readonly isWeb: boolean, + @IIssueMainService public readonly issueMainService: IIssueMainService + ) { + super(); + const targetExtension = data.extensionId ? data.enabledExtensions.find(extension => extension.id.toLocaleLowerCase() === data.extensionId?.toLocaleLowerCase()) : undefined; + this.issueReporterModel = new IssueReporterModel({ + ...data, + issueType: data.issueType || IssueType.Bug, + versionInfo: { + vscodeVersion: `${product.nameShort} ${!!product.darwinUniversalAssetId ? `${product.version} (Universal)` : product.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`, + os: `${this.os.type} ${this.os.arch} ${this.os.release}${isLinuxSnap ? ' snap' : ''}` + }, + extensionsDisabled: !!this.disableExtensions, + fileOnExtension: data.extensionId ? !targetExtension?.isBuiltin : undefined, + selectedExtension: targetExtension + }); + + const fileOnMarketplace = data.issueSource === IssueSource.Marketplace; + const fileOnProduct = data.issueSource === IssueSource.VSCode; + this.issueReporterModel.update({ fileOnMarketplace, fileOnProduct }); + + //TODO: Handle case where extension is not activated + const issueReporterElement = this.getElementById('issue-reporter'); + if (issueReporterElement) { + this.previewButton = new Button(issueReporterElement, unthemedButtonStyles); + const issueRepoName = document.createElement('a'); + issueReporterElement.appendChild(issueRepoName); + issueRepoName.id = 'show-repo-name'; + issueRepoName.classList.add('hidden'); + this.updatePreviewButtonState(); + } + + const issueTitle = data.issueTitle; + if (issueTitle) { + const issueTitleElement = this.getElementById('issue-title'); + if (issueTitleElement) { + issueTitleElement.value = issueTitle; + } + } + + const issueBody = data.issueBody; + if (issueBody) { + const description = this.getElementById('description'); + if (description) { + description.value = issueBody; + this.issueReporterModel.update({ issueDescription: issueBody }); + } + } + + if (this.window.document.documentElement.lang !== 'en') { + show(this.getElementById('english')); + } + + const codiconStyleSheet = createStyleSheet(); + codiconStyleSheet.id = 'codiconStyles'; + + // TODO: Is there a way to use the IThemeService here instead + const iconsStyleSheet = this._register(getIconsStyleSheet(undefined)); + function updateAll() { + codiconStyleSheet.textContent = iconsStyleSheet.getCSS(); + } + + const delayer = new RunOnceScheduler(updateAll, 0); + iconsStyleSheet.onDidChange(() => delayer.schedule()); + delayer.schedule(); + + this.setUpTypes(); + this.applyStyles(data.styles); + + // Handle case where extension is pre-selected through the command + if ((data.data || data.uri) && targetExtension) { + this.updateExtensionStatus(targetExtension); + } + } + + render(): void { + this.renderBlocks(); + } + + setInitialFocus() { + const { fileOnExtension } = this.issueReporterModel.getData(); + if (fileOnExtension) { + const issueTitle = this.window.document.getElementById('issue-title'); + issueTitle?.focus(); + } else { + const issueType = this.window.document.getElementById('issue-type'); + issueType?.focus(); + } + } + + // TODO @justschen: After migration to Aux Window, switch to dedicated css. + private applyStyles(styles: IssueReporterStyles) { + const styleTag = document.createElement('style'); + const content: string[] = []; + + if (styles.inputBackground) { + content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { background-color: ${styles.inputBackground}; }`); + } + + if (styles.inputBorder) { + content.push(`input[type="text"], textarea, select { border: 1px solid ${styles.inputBorder}; }`); + } else { + content.push(`input[type="text"], textarea, select { border: 1px solid transparent; }`); + } + + if (styles.inputForeground) { + content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { color: ${styles.inputForeground}; }`); + } + + if (styles.inputErrorBorder) { + content.push(`.invalid-input, .invalid-input:focus, .validation-error { border: 1px solid ${styles.inputErrorBorder} !important; }`); + content.push(`.required-input { color: ${styles.inputErrorBorder}; }`); + } + + if (styles.inputErrorBackground) { + content.push(`.validation-error { background: ${styles.inputErrorBackground}; }`); + } + + if (styles.inputErrorForeground) { + content.push(`.validation-error { color: ${styles.inputErrorForeground}; }`); + } + + if (styles.inputActiveBorder) { + content.push(`input[type='text']:focus, textarea:focus, select:focus, summary:focus, button:focus, a:focus, .workbenchCommand:focus { border: 1px solid ${styles.inputActiveBorder}; outline-style: none; }`); + } + + if (styles.textLinkColor) { + content.push(`a, .workbenchCommand { color: ${styles.textLinkColor}; }`); + } + + if (styles.textLinkColor) { + content.push(`a { color: ${styles.textLinkColor}; }`); + } + + if (styles.textLinkActiveForeground) { + content.push(`a:hover, .workbenchCommand:hover { color: ${styles.textLinkActiveForeground}; }`); + } + + if (styles.sliderBackgroundColor) { + content.push(`::-webkit-scrollbar-thumb { background-color: ${styles.sliderBackgroundColor}; }`); + } + + if (styles.sliderActiveColor) { + content.push(`::-webkit-scrollbar-thumb:active { background-color: ${styles.sliderActiveColor}; }`); + } + + if (styles.sliderHoverColor) { + content.push(`::--webkit-scrollbar-thumb:hover { background-color: ${styles.sliderHoverColor}; }`); + } + + if (styles.buttonBackground) { + content.push(`.monaco-text-button { background-color: ${styles.buttonBackground} !important; }`); + } + + if (styles.buttonForeground) { + content.push(`.monaco-text-button { color: ${styles.buttonForeground} !important; }`); + } + + if (styles.buttonHoverBackground) { + content.push(`.monaco-text-button:not(.disabled):hover, .monaco-text-button:focus { background-color: ${styles.buttonHoverBackground} !important; }`); + } + + styleTag.textContent = content.join('\n'); + this.window.document.head.appendChild(styleTag); + this.window.document.body.style.color = styles.color || ''; + } + + private async updateIssueReporterUri(extension: IssueReporterExtensionData): Promise { + try { + if (extension.uri) { + const uri = URI.revive(extension.uri); + extension.bugsUrl = uri.toString(); + } + } catch (e) { + this.renderBlocks(); + } + } + + 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(); + this.issueReporterModel.update({ [elementId]: !this.issueReporterModel.getData()[elementId] }); + }); + }); + + const showInfoElements = this.window.document.getElementsByClassName('showInfo'); + for (let i = 0; i < showInfoElements.length; i++) { + const showInfo = showInfoElements.item(i)!; + (showInfo as HTMLAnchorElement).addEventListener('click', (e: MouseEvent) => { + e.preventDefault(); + const label = (e.target); + if (label) { + const containingElement = label.parentElement && label.parentElement.parentElement; + const info = containingElement && containingElement.lastElementChild; + if (info && info.classList.contains('hidden')) { + show(info); + label.textContent = localize('hide', "hide"); + } else { + hide(info); + label.textContent = localize('show', "show"); + } + } + }); + } + + this.addEventListener('issue-source', 'change', (e: Event) => { + const value = (e.target).value; + const problemSourceHelpText = this.getElementById('problem-source-help-text')!; + if (value === '') { + this.issueReporterModel.update({ fileOnExtension: undefined }); + show(problemSourceHelpText); + this.clearSearchResults(); + this.render(); + return; + } else { + hide(problemSourceHelpText); + } + + const descriptionTextArea = this.getElementById('issue-title'); + if (value === IssueSource.VSCode) { + descriptionTextArea.placeholder = localize('vscodePlaceholder', "E.g Workbench is missing problems panel"); + } else if (value === IssueSource.Extension) { + descriptionTextArea.placeholder = localize('extensionPlaceholder', "E.g. Missing alt text on extension readme image"); + } else if (value === IssueSource.Marketplace) { + descriptionTextArea.placeholder = localize('marketplacePlaceholder', "E.g Cannot disable installed extension"); + } else { + descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); + } + + let fileOnExtension, fileOnMarketplace = false; + if (value === IssueSource.Extension) { + fileOnExtension = true; + } else if (value === IssueSource.Marketplace) { + fileOnMarketplace = true; + } + + this.issueReporterModel.update({ fileOnExtension, fileOnMarketplace }); + this.render(); + + const title = (this.getElementById('issue-title')).value; + this.searchIssues(title, fileOnExtension, fileOnMarketplace); + }); + + this.addEventListener('description', 'input', (e: Event) => { + const issueDescription = (e.target).value; + this.issueReporterModel.update({ issueDescription }); + + // Only search for extension issues on title change + if (this.issueReporterModel.fileOnExtension() === false) { + const title = (this.getElementById('issue-title')).value; + this.searchVSCodeIssues(title, issueDescription); + } + }); + + this.addEventListener('issue-title', 'input', (e: Event) => { + const title = (e.target).value; + const lengthValidationMessage = this.getElementById('issue-title-length-validation-error'); + const issueUrl = this.getIssueUrl(); + if (title && this.getIssueUrlWithTitle(title, issueUrl).length > MAX_URL_LENGTH) { + show(lengthValidationMessage); + } else { + hide(lengthValidationMessage); + } + const issueSource = this.getElementById('issue-source'); + if (!issueSource || issueSource.value === '') { + return; + } + + const { fileOnExtension, fileOnMarketplace } = this.issueReporterModel.getData(); + this.searchIssues(title, fileOnExtension, fileOnMarketplace); + }); + } + + public updatePerformanceInfo(info: Partial) { + this.issueReporterModel.update(info); + this.receivedPerformanceInfo = true; + + const state = this.issueReporterModel.getData(); + this.updateProcessInfo(state); + this.updateWorkspaceInfo(state); + this.updatePreviewButtonState(); + } + + public updatePreviewButtonState() { + if (this.isPreviewEnabled()) { + if (this.data.githubAccessToken) { + this.previewButton.label = localize('createOnGitHub', "Create on GitHub"); + } else { + this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub"); + } + this.previewButton.enabled = true; + } else { + this.previewButton.enabled = false; + this.previewButton.label = localize('loadingData', "Loading data..."); + } + + const issueRepoName = this.getElementById('show-repo-name')! as HTMLAnchorElement; + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension && selectedExtension.uri) { + const urlString = URI.revive(selectedExtension.uri).toString(); + issueRepoName.href = urlString; + issueRepoName.addEventListener('click', (e) => this.openLink(e)); + issueRepoName.addEventListener('auxclick', (e) => this.openLink(e)); + const gitHubInfo = this.parseGitHubUrl(urlString); + issueRepoName.textContent = gitHubInfo ? gitHubInfo.owner + '/' + gitHubInfo.repositoryName : urlString; + Object.assign(issueRepoName.style, { + alignSelf: 'flex-end', + display: 'block', + fontSize: '13px', + marginBottom: '10px', + padding: '4px 0px', + textDecoration: 'none', + width: 'auto' + }); + show(issueRepoName); + } else { + // clear styles + issueRepoName.removeAttribute('style'); + hide(issueRepoName); + } + + // Initial check when first opened. + this.getExtensionGitHubUrl(); + } + + private isPreviewEnabled() { + const issueType = this.issueReporterModel.getData().issueType; + + if (this.loadingExtensionData) { + return false; + } + + if (this.isWeb) { + if (issueType === IssueType.FeatureRequest || issueType === IssueType.PerformanceIssue || issueType === IssueType.Bug) { + return true; + } + } else { + if (issueType === IssueType.Bug && this.receivedSystemInfo) { + return true; + } + + if (issueType === IssueType.PerformanceIssue && this.receivedSystemInfo && this.receivedPerformanceInfo) { + return true; + } + } + + return false; + } + + private getExtensionRepositoryUrl(): string | undefined { + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + return selectedExtension && selectedExtension.repositoryUrl; + } + + public getExtensionBugsUrl(): string | undefined { + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + return selectedExtension && selectedExtension.bugsUrl; + } + + public searchVSCodeIssues(title: string, issueDescription?: string): void { + if (title) { + this.searchDuplicates(title, issueDescription); + } else { + this.clearSearchResults(); + } + } + + public searchIssues(title: string, fileOnExtension: boolean | undefined, fileOnMarketplace: boolean | undefined): void { + if (fileOnExtension) { + return this.searchExtensionIssues(title); + } + + if (fileOnMarketplace) { + return this.searchMarketplaceIssues(title); + } + + const description = this.issueReporterModel.getData().issueDescription; + this.searchVSCodeIssues(title, description); + } + + private searchExtensionIssues(title: string): void { + const url = this.getExtensionGitHubUrl(); + if (title) { + const matches = /^https?:\/\/github\.com\/(.*)/.exec(url); + if (matches && matches.length) { + const repo = matches[1]; + return this.searchGitHub(repo, title); + } + + // If the extension has no repository, display empty search results + if (this.issueReporterModel.getData().selectedExtension) { + this.clearSearchResults(); + return this.displaySearchResults([]); + + } + } + + this.clearSearchResults(); + } + + private searchMarketplaceIssues(title: string): void { + if (title) { + const gitHubInfo = this.parseGitHubUrl(this.product.reportMarketplaceIssueUrl!); + if (gitHubInfo) { + return this.searchGitHub(`${gitHubInfo.owner}/${gitHubInfo.repositoryName}`, title); + } + } + } + + public async close(): Promise { + await this.issueMainService.$closeReporter(); + } + + public clearSearchResults(): void { + const similarIssues = this.getElementById('similar-issues')!; + similarIssues.innerText = ''; + this.numberOfSearchResultsDisplayed = 0; + } + + @debounce(300) + private searchGitHub(repo: string, title: string): void { + const query = `is:issue+repo:${repo}+${title}`; + const similarIssues = this.getElementById('similar-issues')!; + + fetch(`https://api.github.com/search/issues?q=${query}`).then((response) => { + response.json().then(result => { + similarIssues.innerText = ''; + if (result && result.items) { + this.displaySearchResults(result.items); + } else { + // If the items property isn't present, the rate limit has been hit + const message = $('div.list-title'); + message.textContent = localize('rateLimited', "GitHub query limit exceeded. Please wait."); + similarIssues.appendChild(message); + + const resetTime = response.headers.get('X-RateLimit-Reset'); + const timeToWait = resetTime ? parseInt(resetTime) - Math.floor(Date.now() / 1000) : 1; + if (this.shouldQueueSearch) { + this.shouldQueueSearch = false; + setTimeout(() => { + this.searchGitHub(repo, title); + this.shouldQueueSearch = true; + }, timeToWait * 1000); + } + } + }).catch(_ => { + console.warn('Timeout or query limit exceeded'); + }); + }).catch(_ => { + console.warn('Error fetching GitHub issues'); + }); + } + + @debounce(300) + private searchDuplicates(title: string, body?: string): void { + const url = 'https://vscode-probot.westus.cloudapp.azure.com:7890/duplicate_candidates'; + const init = { + method: 'POST', + body: JSON.stringify({ + title, + body + }), + headers: new Headers({ + 'Content-Type': 'application/json' + }) + }; + + fetch(url, init).then((response) => { + response.json().then(result => { + this.clearSearchResults(); + + if (result && result.candidates) { + this.displaySearchResults(result.candidates); + } else { + throw new Error('Unexpected response, no candidates property'); + } + }).catch(_ => { + // Ignore + }); + }).catch(_ => { + // Ignore + }); + } + + private displaySearchResults(results: SearchResult[]) { + const similarIssues = this.getElementById('similar-issues')!; + if (results.length) { + const issues = $('div.issues-container'); + const issuesText = $('div.list-title'); + issuesText.textContent = localize('similarIssues', "Similar issues"); + + this.numberOfSearchResultsDisplayed = results.length < 5 ? results.length : 5; + for (let i = 0; i < this.numberOfSearchResultsDisplayed; i++) { + const issue = results[i]; + const link = $('a.issue-link', { href: issue.html_url }); + link.textContent = issue.title; + link.title = issue.title; + link.addEventListener('click', (e) => this.openLink(e)); + link.addEventListener('auxclick', (e) => this.openLink(e)); + + let issueState: HTMLElement; + let item: HTMLElement; + if (issue.state) { + issueState = $('span.issue-state'); + + const issueIcon = $('span.issue-icon'); + issueIcon.appendChild(renderIcon(issue.state === 'open' ? Codicon.issueOpened : Codicon.issueClosed)); + + const issueStateLabel = $('span.issue-state.label'); + issueStateLabel.textContent = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed"); + + issueState.title = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed"); + issueState.appendChild(issueIcon); + issueState.appendChild(issueStateLabel); + + item = $('div.issue', undefined, issueState, link); + } else { + item = $('div.issue', undefined, link); + } + + issues.appendChild(item); + } + + similarIssues.appendChild(issuesText); + similarIssues.appendChild(issues); + } else { + const message = $('div.list-title'); + message.textContent = localize('noSimilarIssues', "No similar issues found"); + similarIssues.appendChild(message); + } + } + + private setUpTypes(): void { + const makeOption = (issueType: IssueType, description: string) => $('option', { 'value': issueType.valueOf() }, escape(description)); + + const typeSelect = this.getElementById('issue-type')! as HTMLSelectElement; + const { issueType } = this.issueReporterModel.getData(); + reset(typeSelect, + makeOption(IssueType.Bug, localize('bugReporter', "Bug Report")), + makeOption(IssueType.FeatureRequest, localize('featureRequest', "Feature Request")), + makeOption(IssueType.PerformanceIssue, localize('performanceIssue', "Performance Issue (freeze, slow, crash)")) + ); + + typeSelect.value = issueType.toString(); + + this.setSourceOptions(); + } + + public makeOption(value: string, description: string, disabled: boolean): HTMLOptionElement { + const option: HTMLOptionElement = document.createElement('option'); + option.disabled = disabled; + option.value = value; + option.textContent = description; + + return option; + } + + public setSourceOptions(): void { + const sourceSelect = this.getElementById('issue-source')! as HTMLSelectElement; + const { issueType, fileOnExtension, selectedExtension, fileOnMarketplace, fileOnProduct } = this.issueReporterModel.getData(); + let selected = sourceSelect.selectedIndex; + if (selected === -1) { + if (fileOnExtension !== undefined) { + selected = fileOnExtension ? 2 : 1; + } else if (selectedExtension?.isBuiltin) { + selected = 1; + } else if (fileOnMarketplace) { + selected = 3; + } else if (fileOnProduct) { + selected = 1; + } + } + + sourceSelect.innerText = ''; + sourceSelect.append(this.makeOption('', localize('selectSource', "Select source"), true)); + sourceSelect.append(this.makeOption(IssueSource.VSCode, localize('vscode', "Visual Studio Code"), false)); + sourceSelect.append(this.makeOption(IssueSource.Extension, localize('extension', "A VS Code extension"), false)); + if (this.product.reportMarketplaceIssueUrl) { + sourceSelect.append(this.makeOption(IssueSource.Marketplace, localize('marketplace', "Extensions Marketplace"), false)); + } + + if (issueType !== IssueType.FeatureRequest) { + sourceSelect.append(this.makeOption(IssueSource.Unknown, localize('unknown', "Don't know"), false)); + } + + if (selected !== -1 && selected < sourceSelect.options.length) { + sourceSelect.selectedIndex = selected; + } else { + sourceSelect.selectedIndex = 0; + hide(this.getElementById('problem-source-help-text')); + } + } + + public renderBlocks(): void { + // Depending on Issue Type, we render different blocks and text + const { issueType, fileOnExtension, fileOnMarketplace, selectedExtension } = this.issueReporterModel.getData(); + const blockContainer = this.getElementById('block-container'); + const systemBlock = this.window.document.querySelector('.block-system'); + const processBlock = this.window.document.querySelector('.block-process'); + const workspaceBlock = this.window.document.querySelector('.block-workspace'); + const extensionsBlock = this.window.document.querySelector('.block-extensions'); + const experimentsBlock = this.window.document.querySelector('.block-experiments'); + const extensionDataBlock = this.window.document.querySelector('.block-extension-data'); + + const problemSource = this.getElementById('problem-source')!; + const descriptionTitle = this.getElementById('issue-description-label')!; + const descriptionSubtitle = this.getElementById('issue-description-subtitle')!; + const extensionSelector = this.getElementById('extension-selection')!; + + const titleTextArea = this.getElementById('issue-title-container')!; + const descriptionTextArea = this.getElementById('description')!; + const extensionDataTextArea = this.getElementById('extension-data')!; + + // Hide all by default + hide(blockContainer); + hide(systemBlock); + hide(processBlock); + hide(workspaceBlock); + hide(extensionsBlock); + hide(experimentsBlock); + hide(extensionSelector); + hide(extensionDataTextArea); + hide(extensionDataBlock); + + show(problemSource); + show(titleTextArea); + show(descriptionTextArea); + + if (fileOnExtension) { + show(extensionSelector); + } + + + if (selectedExtension && this.nonGitHubIssueUrl) { + hide(titleTextArea); + hide(descriptionTextArea); + reset(descriptionTitle, localize('handlesIssuesElsewhere', "This extension handles issues outside of VS Code")); + reset(descriptionSubtitle, localize('elsewhereDescription', "The '{0}' extension prefers to use an external issue reporter. To be taken to that issue reporting experience, click the button below.", selectedExtension.displayName)); + this.previewButton.label = localize('openIssueReporter', "Open External Issue Reporter"); + return; + } + + if (fileOnExtension && selectedExtension?.data) { + const data = selectedExtension?.data; + (extensionDataTextArea as HTMLElement).innerText = data.toString(); + (extensionDataTextArea as HTMLTextAreaElement).readOnly = true; + show(extensionDataBlock); + } + + // only if we know comes from the open reporter command + if (fileOnExtension && this.openReporter) { + (extensionDataTextArea as HTMLTextAreaElement).readOnly = true; + setTimeout(() => { + // delay to make sure from command or not + if (this.openReporter) { + show(extensionDataBlock); + } + }, 100); + show(extensionDataBlock); + } + + if (issueType === IssueType.Bug) { + if (!fileOnMarketplace) { + show(blockContainer); + show(systemBlock); + show(experimentsBlock); + if (!fileOnExtension) { + show(extensionsBlock); + } + } + + reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('bugDescription', "Share the steps needed to reliably reproduce the problem. Please include actual and expected results. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); + } else if (issueType === IssueType.PerformanceIssue) { + if (!fileOnMarketplace) { + show(blockContainer); + show(systemBlock); + show(processBlock); + show(workspaceBlock); + show(experimentsBlock); + } + + if (fileOnExtension) { + show(extensionSelector); + } else if (!fileOnMarketplace) { + show(extensionsBlock); + } + + reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('performanceIssueDesciption', "When did this performance issue happen? Does it occur on startup or after a specific series of actions? We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); + } else if (issueType === IssueType.FeatureRequest) { + reset(descriptionTitle, localize('description', "Description") + ' ', $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('featureRequestDescription', "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); + } + } + + public validateInput(inputId: string): boolean { + const inputElement = (this.getElementById(inputId)); + const inputValidationMessage = this.getElementById(`${inputId}-empty-error`); + const descriptionShortMessage = this.getElementById(`description-short-error`); + if (!inputElement.value) { + inputElement.classList.add('invalid-input'); + inputValidationMessage?.classList.remove('hidden'); + descriptionShortMessage?.classList.add('hidden'); + return false; + } else if (inputId === 'description' && inputElement.value.length < 10) { + inputElement.classList.add('invalid-input'); + descriptionShortMessage?.classList.remove('hidden'); + inputValidationMessage?.classList.add('hidden'); + return false; + } + else { + inputElement.classList.remove('invalid-input'); + inputValidationMessage?.classList.add('hidden'); + if (inputId === 'description') { + descriptionShortMessage?.classList.add('hidden'); + } + return true; + } + } + + public validateInputs(): boolean { + let isValid = true; + ['issue-title', 'description', 'issue-source'].forEach(elementId => { + isValid = this.validateInput(elementId) && isValid; + }); + + if (this.issueReporterModel.fileOnExtension()) { + isValid = this.validateInput('extension-selector') && isValid; + } + + return isValid; + } + + public async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise { + const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`; + const init = { + method: 'POST', + body: JSON.stringify({ + title: issueTitle, + body: issueBody + }), + headers: new Headers({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.data.githubAccessToken}` + }) + }; + + const response = await fetch(url, init); + if (!response.ok) { + console.error('Invalid GitHub URL provided.'); + return false; + } + const result = await response.json(); + this.window.open(result.html_url, '_blank'); + + this.close(); + return true; + } + + public async createIssue(): Promise { + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + const hasUri = this.nonGitHubIssueUrl; + // Short circuit if the extension provides a custom issue handler + if (hasUri) { + const url = this.getExtensionBugsUrl(); + if (url) { + this.hasBeenSubmitted = true; + return true; + } + } + + if (!this.validateInputs()) { + // If inputs are invalid, set focus to the first one and add listeners on them + // to detect further changes + const invalidInput = this.window.document.getElementsByClassName('invalid-input'); + if (invalidInput.length) { + (invalidInput[0]).focus(); + } + + this.addEventListener('issue-title', 'input', _ => { + this.validateInput('issue-title'); + }); + + this.addEventListener('description', 'input', _ => { + this.validateInput('description'); + }); + + this.addEventListener('issue-source', 'change', _ => { + this.validateInput('issue-source'); + }); + + if (this.issueReporterModel.fileOnExtension()) { + this.addEventListener('extension-selector', 'change', _ => { + this.validateInput('extension-selector'); + }); + } + + return false; + } + + this.hasBeenSubmitted = true; + + const issueTitle = (this.getElementById('issue-title')).value; + const issueBody = this.issueReporterModel.serialize(); + + let issueUrl = this.getIssueUrl(); + if (!issueUrl) { + console.error('No issue url found'); + return false; + } + + if (selectedExtension?.uri) { + const uri = URI.revive(selectedExtension.uri); + issueUrl = uri.toString(); + } + + const gitHubDetails = this.parseGitHubUrl(issueUrl); + if (this.data.githubAccessToken && gitHubDetails) { + return this.submitToGitHub(issueTitle, issueBody, gitHubDetails); + } + + const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value, issueUrl); + let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; + + if (url.length > MAX_URL_LENGTH) { + try { + url = await this.writeToClipboard(baseUrl, issueBody); + } catch (_) { + console.error('Writing to clipboard failed'); + return false; + } + } + + this.window.open(url, '_blank'); + + return true; + } + + public async writeToClipboard(baseUrl: string, issueBody: string): Promise { + const shouldWrite = await this.issueMainService.$showClipboardDialog(); + if (!shouldWrite) { + throw new CancellationError(); + } + + return baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`; + } + + public getIssueUrl(): string { + return this.issueReporterModel.fileOnExtension() + ? this.getExtensionGitHubUrl() + : this.issueReporterModel.getData().fileOnMarketplace + ? this.product.reportMarketplaceIssueUrl! + : this.product.reportIssueUrl!; + } + + public parseGitHubUrl(url: string): undefined | { repositoryName: string; owner: string } { + // Assumes a GitHub url to a particular repo, https://github.com/repositoryName/owner. + // Repository name and owner cannot contain '/' + const match = /^https?:\/\/github\.com\/([^\/]*)\/([^\/]*).*/.exec(url); + if (match && match.length) { + return { + owner: match[1], + repositoryName: match[2] + }; + } else { + console.error('No GitHub issues match'); + } + + return undefined; + } + + private getExtensionGitHubUrl(): string { + let repositoryUrl = ''; + const bugsUrl = this.getExtensionBugsUrl(); + const extensionUrl = this.getExtensionRepositoryUrl(); + // If given, try to match the extension's bug url + if (bugsUrl && bugsUrl.match(/^https?:\/\/github\.com\/([^\/]*)\/([^\/]*)\/?(\/issues)?$/)) { + // matches exactly: https://github.com/owner/repo/issues + repositoryUrl = normalizeGitHubUrl(bugsUrl); + } else if (extensionUrl && extensionUrl.match(/^https?:\/\/github\.com\/([^\/]*)\/([^\/]*)$/)) { + // matches exactly: https://github.com/owner/repo + repositoryUrl = normalizeGitHubUrl(extensionUrl); + } else { + this.nonGitHubIssueUrl = true; + repositoryUrl = bugsUrl || extensionUrl || ''; + } + + return repositoryUrl; + } + + public getIssueUrlWithTitle(issueTitle: string, repositoryUrl: string): string { + if (this.issueReporterModel.fileOnExtension()) { + repositoryUrl = repositoryUrl + '/issues/new'; + } + + const queryStringPrefix = repositoryUrl.indexOf('?') === -1 ? '?' : '&'; + return `${repositoryUrl}${queryStringPrefix}title=${encodeURIComponent(issueTitle)}`; + } + + public clearExtensionData(): void { + this.nonGitHubIssueUrl = false; + this.issueReporterModel.update({ extensionData: undefined }); + this.data.issueBody = undefined; + this.data.data = undefined; + this.data.uri = undefined; + } + + public async updateExtensionStatus(extension: IssueReporterExtensionData) { + this.issueReporterModel.update({ selectedExtension: extension }); + + // uses this.configuuration.data to ensure that data is coming from `openReporter` command. + const template = this.data.issueBody; + if (template) { + const descriptionTextArea = this.getElementById('description')!; + const descriptionText = (descriptionTextArea as HTMLTextAreaElement).value; + if (descriptionText === '' || !descriptionText.includes(template.toString())) { + const fullTextArea = descriptionText + (descriptionText === '' ? '' : '\n') + template.toString(); + (descriptionTextArea as HTMLTextAreaElement).value = fullTextArea; + this.issueReporterModel.update({ issueDescription: fullTextArea }); + } + } + + const data = this.data.data; + if (data) { + this.issueReporterModel.update({ extensionData: data }); + extension.data = data; + const extensionDataBlock = this.window.document.querySelector('.block-extension-data')!; + show(extensionDataBlock); + this.renderBlocks(); + } + + const uri = this.data.uri; + if (uri) { + extension.uri = uri; + this.updateIssueReporterUri(extension); + } + + this.validateSelectedExtension(); + const title = (this.getElementById('issue-title')).value; + this.searchExtensionIssues(title); + + this.updatePreviewButtonState(); + this.renderBlocks(); + } + + public validateSelectedExtension(): void { + const extensionValidationMessage = this.getElementById('extension-selection-validation-error')!; + const extensionValidationNoUrlsMessage = this.getElementById('extension-selection-validation-error-no-url')!; + hide(extensionValidationMessage); + hide(extensionValidationNoUrlsMessage); + + const extension = this.issueReporterModel.getData().selectedExtension; + if (!extension) { + this.previewButton.enabled = true; + return; + } + + if (this.loadingExtensionData) { + return; + } + + const hasValidGitHubUrl = this.getExtensionGitHubUrl(); + if (hasValidGitHubUrl) { + this.previewButton.enabled = true; + } else { + this.setExtensionValidationMessage(); + this.previewButton.enabled = false; + } + } + + public setLoading(element: HTMLElement) { + // Show loading + this.openReporter = true; + this.loadingExtensionData = true; + this.updatePreviewButtonState(); + + const extensionDataCaption = this.getElementById('extension-id')!; + hide(extensionDataCaption); + + const extensionDataCaption2 = Array.from(this.window.document.querySelectorAll('.ext-parens')); + extensionDataCaption2.forEach(extensionDataCaption2 => hide(extensionDataCaption2)); + + const showLoading = this.getElementById('ext-loading')!; + show(showLoading); + while (showLoading.firstChild) { + showLoading.removeChild(showLoading.firstChild); + } + showLoading.append(element); + + this.renderBlocks(); + } + + public removeLoading(element: HTMLElement, fromReporter: boolean = false) { + this.openReporter = fromReporter; + this.loadingExtensionData = false; + this.updatePreviewButtonState(); + + const extensionDataCaption = this.getElementById('extension-id')!; + show(extensionDataCaption); + + const extensionDataCaption2 = Array.from(this.window.document.querySelectorAll('.ext-parens')); + extensionDataCaption2.forEach(extensionDataCaption2 => show(extensionDataCaption2)); + + const hideLoading = this.getElementById('ext-loading')!; + hide(hideLoading); + if (hideLoading.firstChild) { + hideLoading.removeChild(element); + } + this.renderBlocks(); + } + + private setExtensionValidationMessage(): void { + const extensionValidationMessage = this.getElementById('extension-selection-validation-error')!; + const extensionValidationNoUrlsMessage = this.getElementById('extension-selection-validation-error-no-url')!; + const bugsUrl = this.getExtensionBugsUrl(); + if (bugsUrl) { + show(extensionValidationMessage); + const link = this.getElementById('extensionBugsLink')!; + link.textContent = bugsUrl; + return; + } + + const extensionUrl = this.getExtensionRepositoryUrl(); + if (extensionUrl) { + show(extensionValidationMessage); + const link = this.getElementById('extensionBugsLink'); + link!.textContent = extensionUrl; + return; + } + + show(extensionValidationNoUrlsMessage); + } + + private updateProcessInfo(state: IssueReporterModelData) { + const target = this.window.document.querySelector('.block-process .block-info') as HTMLElement; + if (target) { + reset(target, $('code', undefined, state.processInfo ?? '')); + } + } + + private updateWorkspaceInfo(state: IssueReporterModelData) { + this.window.document.querySelector('.block-workspace .block-info code')!.textContent = '\n' + state.workspaceInfo; + } + + public updateExtensionTable(extensions: IssueReporterExtensionData[], numThemeExtensions: number): void { + const target = this.window.document.querySelector('.block-extensions .block-info'); + if (target) { + if (this.disableExtensions) { + reset(target, localize('disabledExtensions', "Extensions are disabled")); + return; + } + + const themeExclusionStr = numThemeExtensions ? `\n(${numThemeExtensions} theme extensions excluded)` : ''; + extensions = extensions || []; + + if (!extensions.length) { + target.innerText = 'Extensions: none' + themeExclusionStr; + return; + } + + reset(target, this.getExtensionTableHtml(extensions), document.createTextNode(themeExclusionStr)); + } + } + + private getExtensionTableHtml(extensions: IssueReporterExtensionData[]): HTMLTableElement { + return $('table', undefined, + $('tr', undefined, + $('th', undefined, 'Extension'), + $('th', undefined, 'Author (truncated)' as string), + $('th', undefined, 'Version') + ), + ...extensions.map(extension => $('tr', undefined, + $('td', undefined, extension.name), + $('td', undefined, extension.publisher?.substr(0, 3) ?? 'N/A'), + $('td', undefined, extension.version) + )) + ); + } + + private openLink(event: MouseEvent): void { + event.preventDefault(); + event.stopPropagation(); + // Exclude right click + if (event.which < 3) { + windowOpenNoOpener((event.target).href); + } + } + + public getElementById(elementId: string): T | undefined { + const element = this.window.document.getElementById(elementId) as T | undefined; + if (element) { + return element; + } else { + return undefined; + } + } + + public addEventListener(elementId: string, eventType: string, handler: (event: Event) => void): void { + const element = this.getElementById(elementId); + element?.addEventListener(eventType, handler); + } +} + +// helper functions + +export function hide(el: Element | undefined | null) { + el?.classList.add('hidden'); +} +export function show(el: Element | undefined | null) { + el?.classList.remove('hidden'); +} + + diff --git a/src/vs/code/electron-sandbox/issue/issueReporterModel.ts b/src/vs/code/browser/issue/issueReporterModel.ts similarity index 100% rename from src/vs/code/electron-sandbox/issue/issueReporterModel.ts rename to src/vs/code/browser/issue/issueReporterModel.ts diff --git a/src/vs/code/electron-sandbox/issue/issueReporterPage.ts b/src/vs/code/browser/issue/issueReporterPage.ts similarity index 100% rename from src/vs/code/electron-sandbox/issue/issueReporterPage.ts rename to src/vs/code/browser/issue/issueReporterPage.ts diff --git a/src/vs/code/browser/issue/issueReporterService.ts b/src/vs/code/browser/issue/issueReporterService.ts new file mode 100644 index 00000000000..d724e028e94 --- /dev/null +++ b/src/vs/code/browser/issue/issueReporterService.ts @@ -0,0 +1,193 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { $, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { groupBy } from 'vs/base/common/collections'; +import { isMacintosh } from 'vs/base/common/platform'; +import { IProductConfiguration } from 'vs/base/common/product'; +import { BaseIssueReporterService } from 'vs/code/browser/issue/issue'; +import { localize } from 'vs/nls'; +import { IIssueMainService, IssueReporterData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; + +// GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe. +// ref https://github.com/microsoft/vscode/issues/159191 + +export class IssueWebReporter extends BaseIssueReporterService { + constructor( + disableExtensions: boolean, + data: IssueReporterData, + os: { + type: string; + arch: string; + release: string; + }, + product: IProductConfiguration, + window: Window, + @IIssueMainService issueMainService: IIssueMainService + ) { + super(disableExtensions, data, os, product, window, true, issueMainService); + this.setEventHandlers(); + this.handleExtensionData(data.enabledExtensions); + } + + private handleExtensionData(extensions: IssueReporterExtensionData[]) { + const installedExtensions = extensions.filter(x => !x.isBuiltin); + const { nonThemes, themes } = groupBy(installedExtensions, ext => { + return ext.isTheme ? 'themes' : 'nonThemes'; + }); + + const numberOfThemeExtesions = themes && themes.length; + this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); + this.updateExtensionTable(nonThemes, numberOfThemeExtesions); + if (this.disableExtensions || installedExtensions.length === 0) { + (this.getElementById('disableExtensions')).disabled = true; + } + + this.updateExtensionSelector(installedExtensions); + } + + private async sendReporterMenu(extension: IssueReporterExtensionData): Promise { + try { + const data = await this.issueMainService.$sendReporterMenu(extension.id, extension.name); + return data; + } catch (e) { + console.error(e); + return undefined; + } + } + + public override setEventHandlers(): void { + super.setEventHandlers(); + this.previewButton.onDidClick(async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(); + }); + }); + + this.addEventListener('disableExtensions', 'click', () => { + this.issueMainService.$reloadWithExtensionsDisabled(); + }); + + this.addEventListener('extensionBugsLink', 'click', (e: Event) => { + const url = (e.target).innerText; + windowOpenNoOpener(url); + }); + + this.addEventListener('disableExtensions', 'keydown', (e: Event) => { + e.stopPropagation(); + if ((e as KeyboardEvent).key === 'Enter' || (e as KeyboardEvent).key === ' ') { + this.issueMainService.$reloadWithExtensionsDisabled(); + } + }); + + this.window.document.onkeydown = async (e: KeyboardEvent) => { + const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; + // Cmd/Ctrl+Enter previews issue and closes window + if (cmdOrCtrlKey && e.key === 'Enter') { + this.delayedSubmit.trigger(async () => { + if (await this.createIssue()) { + this.close(); + } + }); + } + + // Cmd/Ctrl + w closes issue window + if (cmdOrCtrlKey && e.key === 'w') { + e.stopPropagation(); + e.preventDefault(); + + const issueTitle = (this.getElementById('issue-title'))!.value; + const { issueDescription } = this.issueReporterModel.getData(); + if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) { + // fire and forget + this.issueMainService.$showConfirmCloseDialog(); + } else { + this.close(); + } + } + + // With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac + // Manually perform the selection + if (isMacintosh) { + if (cmdOrCtrlKey && e.key === 'a' && e.target) { + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + (e.target).select(); + } + } + } + }; + } + + private updateExtensionSelector(extensions: IssueReporterExtensionData[]): void { + interface IOption { + name: string; + id: string; + } + + const extensionOptions: IOption[] = extensions.map(extension => { + return { + name: extension.displayName || extension.name || '', + id: extension.id + }; + }); + + // Sort extensions by name + extensionOptions.sort((a, b) => { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + if (aName > bName) { + return 1; + } + + if (aName < bName) { + return -1; + } + + return 0; + }); + + const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => { + const selected = selectedExtension && extension.id === selectedExtension.id; + return $('option', { + 'value': extension.id, + 'selected': selected || '' + }, extension.name); + }; + + const extensionsSelector = this.getElementById('extension-selector'); + if (extensionsSelector) { + const { selectedExtension } = this.issueReporterModel.getData(); + reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); + + if (!selectedExtension) { + extensionsSelector.selectedIndex = 0; + } + + this.addEventListener('extension-selector', 'change', async (e: Event) => { + this.clearExtensionData(); + const selectedExtensionId = (e.target).value; + this.selectedExtension = selectedExtensionId; + const extensions = this.issueReporterModel.getData().allExtensions; + const matches = extensions.filter(extension => extension.id === selectedExtensionId); + if (matches.length) { + this.issueReporterModel.update({ selectedExtension: matches[0] }); + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension) { + await this.sendReporterMenu(selectedExtension); + } else { + this.issueReporterModel.update({ selectedExtension: undefined }); + this.clearSearchResults(); + this.clearExtensionData(); + this.validateSelectedExtension(); + this.updateExtensionStatus(matches[0]); + } + } + }); + } + + this.addEventListener('problem-source', 'change', (_) => { + this.validateSelectedExtension(); + }); + } +} diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index f0f6dc535a4..46193cdbe97 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -105,7 +105,7 @@ import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/e import { UserDataProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataProfilesHandler'; import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc'; import { Promises, RunOnceScheduler, runWhenGlobalIdle } from 'vs/base/common/async'; -import { resolveMachineId, resolveSqmId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; +import { resolveMachineId, resolveSqmId, resolvedevDeviceId } from 'vs/platform/telemetry/electron-main/telemetryUtils'; import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; import { LoggerChannel } from 'vs/platform/log/electron-main/logIpc'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; @@ -611,17 +611,18 @@ export class CodeApplication extends Disposable { // Resolve unique machine ID this.logService.trace('Resolving machine identifier...'); - const [machineId, sqmId] = await Promise.all([ + const [machineId, sqmId, devDeviceId] = await Promise.all([ resolveMachineId(this.stateService, this.logService), - resolveSqmId(this.stateService, this.logService) + resolveSqmId(this.stateService, this.logService), + resolvedevDeviceId(this.stateService, this.logService) ]); this.logService.trace(`Resolved machine identifier: ${machineId}`); // Shared process - const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId, sqmId); + const { sharedProcessReady, sharedProcessClient } = this.setupSharedProcess(machineId, sqmId, devDeviceId); // Services - const appInstantiationService = await this.initServices(machineId, sqmId, sharedProcessReady); + const appInstantiationService = await this.initServices(machineId, sqmId, devDeviceId, sharedProcessReady); // Auth Handler this._register(appInstantiationService.createInstance(ProxyAuthHandler)); @@ -986,8 +987,10 @@ export class CodeApplication extends Disposable { return false; } - private setupSharedProcess(machineId: string, sqmId: string): { sharedProcessReady: Promise; sharedProcessClient: Promise } { - const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId)); + private setupSharedProcess(machineId: string, sqmId: string, devDeviceId: string): { sharedProcessReady: Promise; sharedProcessClient: Promise } { + const sharedProcess = this._register(this.mainInstantiationService.createInstance(SharedProcess, machineId, sqmId, devDeviceId)); + + this._register(sharedProcess.onDidCrash(() => this.windowsMainService?.sendToFocused('vscode:reportSharedProcessCrash'))); const sharedProcessClient = (async () => { this.logService.trace('Main->SharedProcess#connect'); @@ -1008,7 +1011,7 @@ export class CodeApplication extends Disposable { return { sharedProcessReady, sharedProcessClient }; } - private async initServices(machineId: string, sqmId: string, sharedProcessReady: Promise): Promise { + private async initServices(machineId: string, sqmId: string, devDeviceId: string, sharedProcessReady: Promise): Promise { const services = new ServiceCollection(); // Update @@ -1031,7 +1034,7 @@ export class CodeApplication extends Disposable { } // Windows - services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, this.userEnv], false)); + services.set(IWindowsMainService, new SyncDescriptor(WindowsMainService, [machineId, sqmId, devDeviceId, this.userEnv], false)); services.set(IAuxiliaryWindowsMainService, new SyncDescriptor(AuxiliaryWindowsMainService, undefined, false)); // Dialogs @@ -1111,7 +1114,7 @@ export class CodeApplication extends Disposable { const isInternal = isInternalTelemetry(this.productService, this.configurationService); const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, isInternal); + const commonProperties = resolveCommonProperties(release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, sqmId, devDeviceId, isInternal); const piiPaths = getPiiPathsFromEnvironment(this.environmentMainService); const config: ITelemetryServiceConfig = { appenders: [appender], commonProperties, piiPaths, sendErrorTelemetry: true }; diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index 2cbe4b0f8d2..c72367bff1a 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -6,7 +6,7 @@ import { safeInnerHtml } from 'vs/base/browser/dom'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { isLinux, isWindows } from 'vs/base/common/platform'; -import BaseHtml from 'vs/code/electron-sandbox/issue/issueReporterPage'; +import BaseHtml from 'vs/code/browser/issue/issueReporterPage'; import 'vs/css!./media/issueReporter'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; @@ -18,7 +18,7 @@ import { registerMainProcessRemoteService } from 'vs/platform/ipc/electron-sandb import { IIssueMainService, IssueReporterWindowConfiguration } from 'vs/platform/issue/common/issue'; import { INativeHostService } from 'vs/platform/native/common/native'; import { NativeHostService } from 'vs/platform/native/common/nativeHostService'; -import { IssueReporter } from './issueReporterService'; +import { IssueReporter2 } from 'vs/code/electron-sandbox/issue/issueReporterService2'; import { mainWindow } from 'vs/base/browser/window'; export function startup(configuration: IssueReporterWindowConfiguration) { @@ -29,7 +29,7 @@ export function startup(configuration: IssueReporterWindowConfiguration) { const instantiationService = initServices(configuration.windowId); - const issueReporter = instantiationService.createInstance(IssueReporter, configuration); + const issueReporter = instantiationService.createInstance(IssueReporter2, configuration); issueReporter.render(); mainWindow.document.body.style.display = 'block'; issueReporter.setInitialFocus(); diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 6edb382d5ac..a295a881487 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -16,7 +16,7 @@ import { isLinuxSnap, isMacintosh } from 'vs/base/common/platform'; import { escape } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/electron-sandbox/issue/issueReporterModel'; +import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/browser/issue/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'; diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService2.ts b/src/vs/code/electron-sandbox/issue/issueReporterService2.ts new file mode 100644 index 00000000000..51bdfe923b8 --- /dev/null +++ b/src/vs/code/electron-sandbox/issue/issueReporterService2.ts @@ -0,0 +1,508 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { $, reset, windowOpenNoOpener } from 'vs/base/browser/dom'; +import { mainWindow } from 'vs/base/browser/window'; +import { Codicon } from 'vs/base/common/codicons'; +import { groupBy } from 'vs/base/common/collections'; +import { CancellationError } from 'vs/base/common/errors'; +import { isMacintosh } from 'vs/base/common/platform'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { IssueReporterData as IssueReporterModelData } from 'vs/code/browser/issue/issueReporterModel'; +import { BaseIssueReporterService, hide, show } from 'vs/code/browser/issue/issue'; +import { localize } from 'vs/nls'; +import { isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; +import { IIssueMainService, IssueReporterData, IssueReporterExtensionData, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; +import { INativeHostService } from 'vs/platform/native/common/native'; +import { applyZoom, zoomIn, zoomOut } from 'vs/platform/window/electron-sandbox/window'; + +// GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe. +// ref https://github.com/microsoft/vscode/issues/159191 +const MAX_URL_LENGTH = 7500; + + +export class IssueReporter2 extends BaseIssueReporterService { + constructor( + private readonly configuration: IssueReporterWindowConfiguration, + @INativeHostService private readonly nativeHostService: INativeHostService, + @IIssueMainService issueMainService: IIssueMainService + ) { + super(configuration.disableExtensions, configuration.data, configuration.os, configuration.product, mainWindow, false, issueMainService); + + this.issueMainService.$getSystemInfo().then(info => { + this.issueReporterModel.update({ systemInfo: info }); + this.receivedSystemInfo = true; + + this.updateSystemInfo(this.issueReporterModel.getData()); + this.updatePreviewButtonState(); + }); + if (configuration.data.issueType === IssueType.PerformanceIssue) { + this.issueMainService.$getPerformanceInfo().then(info => { + this.updatePerformanceInfo(info as Partial); + }); + } + + this.setEventHandlers(); + applyZoom(configuration.data.zoomLevel, mainWindow); + this.handleExtensionData(configuration.data.enabledExtensions); + this.updateExperimentsInfo(configuration.data.experiments); + this.updateRestrictedMode(configuration.data.restrictedMode); + this.updateUnsupportedMode(configuration.data.isUnsupported); + } + + private handleExtensionData(extensions: IssueReporterExtensionData[]) { + const installedExtensions = extensions.filter(x => !x.isBuiltin); + const { nonThemes, themes } = groupBy(installedExtensions, ext => { + return ext.isTheme ? 'themes' : 'nonThemes'; + }); + + const numberOfThemeExtesions = themes && themes.length; + this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); + this.updateExtensionTable(nonThemes, numberOfThemeExtesions); + if (this.disableExtensions || installedExtensions.length === 0) { + (this.getElementById('disableExtensions')).disabled = true; + } + + this.updateExtensionSelector(installedExtensions); + } + + private async sendReporterMenu(extension: IssueReporterExtensionData): Promise { + try { + const data = await this.issueMainService.$sendReporterMenu(extension.id, extension.name); + return data; + } catch (e) { + console.error(e); + return undefined; + } + } + + public override setEventHandlers(): void { + super.setEventHandlers(); + + // Keep all event listerns involving window and issue creation + this.previewButton.onDidClick(async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(); + }); + }); + + this.addEventListener('disableExtensions', 'click', () => { + this.issueMainService.$reloadWithExtensionsDisabled(); + }); + + this.addEventListener('extensionBugsLink', 'click', (e: Event) => { + const url = (e.target).innerText; + windowOpenNoOpener(url); + }); + + this.addEventListener('disableExtensions', 'keydown', (e: Event) => { + e.stopPropagation(); + if ((e as KeyboardEvent).keyCode === 13 || (e as KeyboardEvent).keyCode === 32) { + this.issueMainService.$reloadWithExtensionsDisabled(); + } + }); + + + // THIS IS THE MAIN IMPORTANT PART + mainWindow.document.onkeydown = async (e: KeyboardEvent) => { + const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; + // Cmd/Ctrl+Enter previews issue and closes window + if (cmdOrCtrlKey && e.key === 'Enter') { + this.delayedSubmit.trigger(async () => { + if (await this.createIssue()) { + this.close(); + } + }); + } + + // Cmd/Ctrl + w closes issue window + if (cmdOrCtrlKey && e.key === 'w') { + e.stopPropagation(); + e.preventDefault(); + + const issueTitle = (this.getElementById('issue-title'))!.value; + const { issueDescription } = this.issueReporterModel.getData(); + if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) { + // fire and forget + this.issueMainService.$showConfirmCloseDialog(); + } else { + this.close(); + } + } + + // Cmd/Ctrl + zooms in + if (cmdOrCtrlKey && (e.key === '+' || e.key === '=')) { + zoomIn(mainWindow); + } + + // Cmd/Ctrl - zooms out + if (cmdOrCtrlKey && e.key === '-') { + zoomOut(mainWindow); + } + + // With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac + // Manually perform the selection + if (isMacintosh) { + if (cmdOrCtrlKey && e.key === 'a' && e.target) { + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { + (e.target).select(); + } + } + } + }; + } + + public override async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise { + const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`; + const init = { + method: 'POST', + body: JSON.stringify({ + title: issueTitle, + body: issueBody + }), + headers: new Headers({ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.data.githubAccessToken}` + }) + }; + + const response = await fetch(url, init); + if (!response.ok) { + console.error('Invalid GitHub URL provided.'); + return false; + } + const result = await response.json(); + await this.nativeHostService.openExternal(result.html_url); + this.close(); + return true; + } + + public override async createIssue(): Promise { + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + const hasUri = this.nonGitHubIssueUrl; + // Short circuit if the extension provides a custom issue handler + if (hasUri) { + const url = this.getExtensionBugsUrl(); + if (url) { + this.hasBeenSubmitted = true; + await this.nativeHostService.openExternal(url); + return true; + } + } + + if (!this.validateInputs()) { + // If inputs are invalid, set focus to the first one and add listeners on them + // to detect further changes + const invalidInput = mainWindow.document.getElementsByClassName('invalid-input'); + if (invalidInput.length) { + (invalidInput[0]).focus(); + } + + this.addEventListener('issue-title', 'input', _ => { + this.validateInput('issue-title'); + }); + + this.addEventListener('description', 'input', _ => { + this.validateInput('description'); + }); + + this.addEventListener('issue-source', 'change', _ => { + this.validateInput('issue-source'); + }); + + if (this.issueReporterModel.fileOnExtension()) { + this.addEventListener('extension-selector', 'change', _ => { + this.validateInput('extension-selector'); + }); + } + + return false; + } + + this.hasBeenSubmitted = true; + + const issueTitle = (this.getElementById('issue-title')).value; + const issueBody = this.issueReporterModel.serialize(); + + let issueUrl = this.getIssueUrl(); + if (!issueUrl) { + console.error('No issue url found'); + return false; + } + + if (selectedExtension?.uri) { + const uri = URI.revive(selectedExtension.uri); + issueUrl = uri.toString(); + } + + const gitHubDetails = this.parseGitHubUrl(issueUrl); + if (this.data.githubAccessToken && gitHubDetails) { + return this.submitToGitHub(issueTitle, issueBody, gitHubDetails); + } + + const baseUrl = this.getIssueUrlWithTitle((this.getElementById('issue-title')).value, issueUrl); + let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`; + + if (url.length > MAX_URL_LENGTH) { + try { + url = await this.writeToClipboard(baseUrl, issueBody); + } catch (_) { + console.error('Writing to clipboard failed'); + return false; + } + } + + await this.nativeHostService.openExternal(url); + return true; + } + + public override async writeToClipboard(baseUrl: string, issueBody: string): Promise { + const shouldWrite = await this.issueMainService.$showClipboardDialog(); + if (!shouldWrite) { + throw new CancellationError(); + } + + await this.nativeHostService.writeClipboardText(issueBody); + + return baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`; + } + + private updateSystemInfo(state: IssueReporterModelData) { + const target = mainWindow.document.querySelector('.block-system .block-info'); + + if (target) { + const systemInfo = state.systemInfo!; + const renderedDataTable = $('table', undefined, + $('tr', undefined, + $('td', undefined, 'CPUs'), + $('td', undefined, systemInfo.cpus || '') + ), + $('tr', undefined, + $('td', undefined, 'GPU Status' as string), + $('td', undefined, Object.keys(systemInfo.gpuStatus).map(key => `${key}: ${systemInfo.gpuStatus[key]}`).join('\n')) + ), + $('tr', undefined, + $('td', undefined, 'Load (avg)' as string), + $('td', undefined, systemInfo.load || '') + ), + $('tr', undefined, + $('td', undefined, 'Memory (System)' as string), + $('td', undefined, systemInfo.memory) + ), + $('tr', undefined, + $('td', undefined, 'Process Argv' as string), + $('td', undefined, systemInfo.processArgs) + ), + $('tr', undefined, + $('td', undefined, 'Screen Reader' as string), + $('td', undefined, systemInfo.screenReader) + ), + $('tr', undefined, + $('td', undefined, 'VM'), + $('td', undefined, systemInfo.vmHint) + ) + ); + reset(target, renderedDataTable); + + systemInfo.remoteData.forEach(remote => { + target.appendChild($('hr')); + if (isRemoteDiagnosticError(remote)) { + const remoteDataTable = $('table', undefined, + $('tr', undefined, + $('td', undefined, 'Remote'), + $('td', undefined, remote.hostName) + ), + $('tr', undefined, + $('td', undefined, ''), + $('td', undefined, remote.errorMessage) + ) + ); + target.appendChild(remoteDataTable); + } else { + const remoteDataTable = $('table', undefined, + $('tr', undefined, + $('td', undefined, 'Remote'), + $('td', undefined, remote.latency ? `${remote.hostName} (latency: ${remote.latency.current.toFixed(2)}ms last, ${remote.latency.average.toFixed(2)}ms average)` : remote.hostName) + ), + $('tr', undefined, + $('td', undefined, 'OS'), + $('td', undefined, remote.machineInfo.os) + ), + $('tr', undefined, + $('td', undefined, 'CPUs'), + $('td', undefined, remote.machineInfo.cpus || '') + ), + $('tr', undefined, + $('td', undefined, 'Memory (System)' as string), + $('td', undefined, remote.machineInfo.memory) + ), + $('tr', undefined, + $('td', undefined, 'VM'), + $('td', undefined, remote.machineInfo.vmHint) + ) + ); + target.appendChild(remoteDataTable); + } + }); + } + } + + public updateExtensionSelector(extensions: IssueReporterExtensionData[]): void { + interface IOption { + name: string; + id: string; + } + + const extensionOptions: IOption[] = extensions.map(extension => { + return { + name: extension.displayName || extension.name || '', + id: extension.id + }; + }); + + // Sort extensions by name + extensionOptions.sort((a, b) => { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + if (aName > bName) { + return 1; + } + + if (aName < bName) { + return -1; + } + + return 0; + }); + + const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => { + const selected = selectedExtension && extension.id === selectedExtension.id; + return $('option', { + 'value': extension.id, + 'selected': selected || '' + }, extension.name); + }; + + const extensionsSelector = this.getElementById('extension-selector'); + if (extensionsSelector) { + const { selectedExtension } = this.issueReporterModel.getData(); + reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); + + if (!selectedExtension) { + extensionsSelector.selectedIndex = 0; + } + + this.addEventListener('extension-selector', 'change', async (e: Event) => { + this.clearExtensionData(); + const selectedExtensionId = (e.target).value; + this.selectedExtension = selectedExtensionId; + const extensions = this.issueReporterModel.getData().allExtensions; + const matches = extensions.filter(extension => extension.id === selectedExtensionId); + if (matches.length) { + this.issueReporterModel.update({ selectedExtension: matches[0] }); + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension) { + const iconElement = document.createElement('span'); + iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); + this.setLoading(iconElement); + const openReporterData = await this.sendReporterMenu(selectedExtension); + if (openReporterData) { + if (this.selectedExtension === selectedExtensionId) { + this.removeLoading(iconElement, true); + this.configuration.data = openReporterData; + this.data = openReporterData; + } else if (this.selectedExtension !== selectedExtensionId) { + } + } + else { + if (!this.loadingExtensionData) { + iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin'); + } + this.removeLoading(iconElement); + // if not using command, should have no configuration data in fields we care about and check later. + this.clearExtensionData(); + + // case when previous extension was opened from normal openIssueReporter command + selectedExtension.data = undefined; + selectedExtension.uri = undefined; + } + if (this.selectedExtension === selectedExtensionId) { + // repopulates the fields with the new data given the selected extension. + this.updateExtensionStatus(matches[0]); + this.openReporter = false; + } + } else { + this.issueReporterModel.update({ selectedExtension: undefined }); + this.clearSearchResults(); + this.clearExtensionData(); + this.validateSelectedExtension(); + this.updateExtensionStatus(matches[0]); + } + } + }); + } + + this.addEventListener('problem-source', 'change', (_) => { + this.validateSelectedExtension(); + }); + } + + public override setLoading(element: HTMLElement) { + // Show loading + this.openReporter = true; + this.loadingExtensionData = true; + this.updatePreviewButtonState(); + + const extensionDataCaption = this.getElementById('extension-id')!; + hide(extensionDataCaption); + + const extensionDataCaption2 = Array.from(mainWindow.document.querySelectorAll('.ext-parens')); + extensionDataCaption2.forEach(extensionDataCaption2 => hide(extensionDataCaption2)); + + const showLoading = this.getElementById('ext-loading')!; + show(showLoading); + while (showLoading.firstChild) { + showLoading.removeChild(showLoading.firstChild); + } + showLoading.append(element); + + this.renderBlocks(); + } + + public override removeLoading(element: HTMLElement, fromReporter: boolean = false) { + this.openReporter = fromReporter; + this.loadingExtensionData = false; + this.updatePreviewButtonState(); + + const extensionDataCaption = this.getElementById('extension-id')!; + show(extensionDataCaption); + + const extensionDataCaption2 = Array.from(mainWindow.document.querySelectorAll('.ext-parens')); + extensionDataCaption2.forEach(extensionDataCaption2 => show(extensionDataCaption2)); + + const hideLoading = this.getElementById('ext-loading')!; + hide(hideLoading); + if (hideLoading.firstChild) { + hideLoading.removeChild(element); + } + this.renderBlocks(); + } + + private updateRestrictedMode(restrictedMode: boolean) { + this.issueReporterModel.update({ restrictedMode }); + } + + private updateUnsupportedMode(isUnsupported: boolean) { + this.issueReporterModel.update({ isUnsupported }); + } + + private updateExperimentsInfo(experimentInfo: string | undefined) { + this.issueReporterModel.update({ experimentInfo }); + const target = mainWindow.document.querySelector('.block-experiments .block-info'); + if (target) { + target.textContent = experimentInfo ? experimentInfo : localize('noCurrentExperiments', "No current experiments."); + } + } +} diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index aea83578ee0..2c1d7afc54c 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -57,7 +57,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { UserDataProfilesReadonlyService } from 'vs/platform/userDataProfile/node/userDataProfile'; -import { resolveMachineId, resolveSqmId } from 'vs/platform/telemetry/node/telemetryUtils'; +import { resolveMachineId, resolveSqmId, resolvedevDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; @@ -186,6 +186,7 @@ class CliMain extends Disposable { } } const sqmId = await resolveSqmId(stateService, logService); + const devDeviceId = await resolvedevDeviceId(stateService, logService); // Initialize user data profiles after initializing the state userDataProfilesService.init(); @@ -221,7 +222,7 @@ class CliMain extends Disposable { const config: ITelemetryServiceConfig = { appenders, sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, machineId, sqmId, isInternal), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, machineId, sqmId, devDeviceId, isInternal), piiPaths: getPiiPathsFromEnvironment(environmentService) }; diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 9a59ccce5c3..f8e915491fc 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -307,7 +307,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { telemetryService = new TelemetryService({ appenders, - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, internalTelemetry), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, this.configuration.sqmId, this.configuration.devDeviceId, internalTelemetry), sendErrorTelemetry: true, piiPaths: getPiiPathsFromEnvironment(environmentService), }, configurationService, productService); diff --git a/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts b/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts index be9c47d5bc4..1708f3a07a1 100644 --- a/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts +++ b/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IssueReporterModel } from 'vs/code/electron-sandbox/issue/issueReporterModel'; +import { IssueReporterModel } from 'vs/code/browser/issue/issueReporterModel'; import { IssueType } from 'vs/platform/issue/common/issue'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index cbaa5f01d3b..2985f959335 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -250,11 +250,21 @@ export interface IOverlayWidgetPosition { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: Event; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ diff --git a/src/vs/editor/browser/services/hoverService/hoverService.ts b/src/vs/editor/browser/services/hoverService/hoverService.ts index bd74d1b3d67..8c5e7319331 100644 --- a/src/vs/editor/browser/services/hoverService/hoverService.ts +++ b/src/vs/editor/browser/services/hoverService/hoverService.ts @@ -62,7 +62,9 @@ export class HoverService extends Disposable implements IHoverService { // HACK, remove this check when #189076 is fixed if (!skipLastFocusedUpdate) { if (trapFocus && activeElement) { - this._lastFocusedElementBeforeOpen = activeElement as HTMLElement; + if (!activeElement.classList.contains('monaco-hover')) { + this._lastFocusedElementBeforeOpen = activeElement as HTMLElement; + } } else { this._lastFocusedElementBeforeOpen = undefined; } @@ -187,6 +189,8 @@ export class HoverService extends Disposable implements IHoverService { } } + private readonly _existingHovers = 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 { @@ -216,15 +220,13 @@ export class HoverService extends Disposable implements IHoverService { hoverDelegate.onDidHideHover?.(); hoverWidget = undefined; } - htmlElement.removeAttribute('custom-hover-active'); }; - const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget) => { + 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); - await hoverWidget.update(typeof content === 'function' ? content() : content, focus, options); - htmlElement.setAttribute('custom-hover-active', 'true'); + await hoverWidget.update(typeof content === 'function' ? content() : content, focus, { ...options, trapFocus }); } }, delay); }; @@ -299,7 +301,7 @@ export class HoverService extends Disposable implements IHoverService { const hover: IUpdatableHover = { show: focus => { hideHover(false, true); // terminate a ongoing mouse over preparation - triggerShowHover(0, focus); // show hover immediately + triggerShowHover(0, focus, undefined, focus); // show hover immediately }, hide: () => { hideHover(true, true); @@ -309,6 +311,7 @@ export class HoverService extends Disposable implements IHoverService { await hoverWidget?.update(content, undefined, hoverOptions); }, dispose: () => { + this._existingHovers.delete(htmlElement); mouseOverDomEmitter.dispose(); mouseLeaveEmitter.dispose(); mouseDownEmitter.dispose(); @@ -317,8 +320,21 @@ export class HoverService extends Disposable implements IHoverService { hideHover(true, true); } }; + this._existingHovers.set(htmlElement, hover); return hover; } + + triggerUpdatableHover(target: HTMLElement): void { + const hover = this._existingHovers.get(target); + if (hover) { + hover.show(true); + } + } + + public override dispose(): void { + this._existingHovers.forEach(hover => hover.dispose()); + super.dispose(); + } } function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOptions | number | string | undefined { diff --git a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts index 869e493c1f9..762b6626aff 100644 --- a/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts +++ b/src/vs/editor/browser/services/hoverService/updatableHoverWidget.ts @@ -42,7 +42,7 @@ export class UpdatableHoverWidget implements IDisposable { // show 'Loading' if no hover is up yet if (!this._hoverWidget) { - this.show(localize('iconLabel.loading', "Loading..."), focus); + this.show(localize('iconLabel.loading', "Loading..."), focus, options); } // compute the content diff --git a/src/vs/editor/browser/stableEditorScroll.ts b/src/vs/editor/browser/stableEditorScroll.ts index fe02be80430..986e18ac6c0 100644 --- a/src/vs/editor/browser/stableEditorScroll.ts +++ b/src/vs/editor/browser/stableEditorScroll.ts @@ -5,6 +5,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; +import { ScrollType } from 'vs/editor/common/editorCommon'; export class StableEditorScrollState { @@ -59,7 +60,7 @@ export class StableEditorScrollState { } const offset = editor.getTopForLineNumber(currentCursorPosition.lineNumber) - editor.getTopForLineNumber(this._cursorPosition.lineNumber); - editor.setScrollTop(editor.getScrollTop() + offset); + editor.setScrollTop(editor.getScrollTop() + offset, ScrollType.Immediate); } } @@ -78,7 +79,7 @@ export class StableEditorBottomScrollState { if (visibleRanges.length > 0) { visiblePosition = visibleRanges.at(-1)!.getEndPosition(); const visiblePositionScrollBottom = editor.getBottomForLineNumber(visiblePosition.lineNumber); - visiblePositionScrollDelta = (editor.getScrollTop() + editor.getLayoutInfo().height) - visiblePositionScrollBottom; + visiblePositionScrollDelta = visiblePositionScrollBottom - editor.getScrollTop(); } return new StableEditorBottomScrollState(editor.getScrollTop(), editor.getContentHeight(), visiblePosition, visiblePositionScrollDelta); } @@ -99,7 +100,7 @@ export class StableEditorBottomScrollState { if (this._visiblePosition) { const visiblePositionScrollBottom = editor.getBottomForLineNumber(this._visiblePosition.lineNumber); - editor.setScrollTop(visiblePositionScrollBottom - (this._visiblePositionScrollDelta + editor.getLayoutInfo().height)); + editor.setScrollTop(visiblePositionScrollBottom - this._visiblePositionScrollDelta, ScrollType.Immediate); } } } diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index aaef0a1deb3..564040783d2 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -641,8 +641,7 @@ export class View extends ViewEventHandler { } public layoutOverlayWidget(widgetData: IOverlayWidgetData): void { - const newPreference = widgetData.position ? widgetData.position.preference : null; - const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); + const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, widgetData.position); if (shouldRender) { this._scheduleRender(); } diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index 3112c19c324..a11435b8e73 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -167,11 +167,19 @@ interface IBoxLayoutResult { left: number; } -interface IRenderData { +interface IOffViewportRenderData { + kind: 'offViewport'; + preserveFocus: boolean; +} + +interface IInViewportRenderData { + kind: 'inViewport'; coordinate: Coordinate; position: ContentWidgetPositionPreference; } +type IRenderData = IInViewportRenderData | IOffViewportRenderData; + class Widget { private readonly _context: ViewContext; private readonly _viewDomNode: FastDomNode; @@ -435,7 +443,11 @@ class Widget { const { primary, secondary } = this._getAnchorsCoordinates(ctx); if (!primary) { - return null; + return { + kind: 'offViewport', + preserveFocus: this.domNode.domNode.contains(this.domNode.domNode.ownerDocument.activeElement) + }; + // return null; } if (this._cachedDomNodeOffsetWidth === -1 || this._cachedDomNodeOffsetHeight === -1) { @@ -474,7 +486,11 @@ class Widget { return null; } if (pass === 2 || placement.fitsAbove) { - return { coordinate: new Coordinate(placement.aboveTop, placement.left), position: ContentWidgetPositionPreference.ABOVE }; + return { + kind: 'inViewport', + coordinate: new Coordinate(placement.aboveTop, placement.left), + position: ContentWidgetPositionPreference.ABOVE + }; } } else if (pref === ContentWidgetPositionPreference.BELOW) { if (!placement) { @@ -482,13 +498,25 @@ class Widget { return null; } if (pass === 2 || placement.fitsBelow) { - return { coordinate: new Coordinate(placement.belowTop, placement.left), position: ContentWidgetPositionPreference.BELOW }; + return { + kind: 'inViewport', + coordinate: new Coordinate(placement.belowTop, placement.left), + position: ContentWidgetPositionPreference.BELOW + }; } } else { if (this.allowEditorOverflow) { - return { coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(new Coordinate(anchor.top, anchor.left)), position: ContentWidgetPositionPreference.EXACT }; + return { + kind: 'inViewport', + coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(new Coordinate(anchor.top, anchor.left)), + position: ContentWidgetPositionPreference.EXACT + }; } else { - return { coordinate: new Coordinate(anchor.top, anchor.left), position: ContentWidgetPositionPreference.EXACT }; + return { + kind: 'inViewport', + coordinate: new Coordinate(anchor.top, anchor.left), + position: ContentWidgetPositionPreference.EXACT + }; } } } @@ -518,12 +546,19 @@ class Widget { } public render(ctx: RestrictedRenderingContext): void { - if (!this._renderData) { + if (!this._renderData || this._renderData.kind === 'offViewport') { // This widget should be invisible if (this._isVisible) { this.domNode.removeAttribute('monaco-visible-content-widget'); this._isVisible = false; - this.domNode.setVisibility('hidden'); + + if (this._renderData?.kind === 'offViewport' && this._renderData.preserveFocus) { + // widget wants to be shown, but it is outside of the viewport and it + // has focus which we need to preserve + this.domNode.setTop(-1000); + } else { + this.domNode.setVisibility('hidden'); + } } if (typeof this._actual.afterRender === 'function') { diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index 0953248e2ab..5b3e86a042d 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -5,7 +5,7 @@ import 'vs/css!./overlayWidgets'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { IOverlayWidget, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; @@ -17,6 +17,7 @@ import * as dom from 'vs/base/browser/dom'; interface IWidgetData { widget: IOverlayWidget; preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + stack?: number; domNode: FastDomNode; } @@ -109,14 +110,17 @@ export class ViewOverlayWidgets extends ViewPart { this._updateMaxMinWidth(); } - public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null): boolean { + public setWidgetPosition(widget: IOverlayWidget, position: IOverlayWidgetPosition | null): boolean { const widgetData = this._widgets[widget.getId()]; - if (widgetData.preference === preference) { + const preference = position ? position.preference : null; + const stack = position?.stackOridinal; + if (widgetData.preference === preference && widgetData.stack === stack) { this._updateMaxMinWidth(); return false; } widgetData.preference = preference; + widgetData.stack = stack; this.setShouldRender(); this._updateMaxMinWidth(); @@ -150,7 +154,7 @@ export class ViewOverlayWidgets extends ViewPart { this._context.viewLayout.setOverlayWidgetsMinWidth(maxMinWidth); } - private _renderWidget(widgetData: IWidgetData): void { + private _renderWidget(widgetData: IWidgetData, stackCoordinates: number[]): void { const domNode = widgetData.domNode; if (widgetData.preference === null) { @@ -158,16 +162,29 @@ export class ViewOverlayWidgets extends ViewPart { return; } - if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER) { - domNode.setTop(0); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); - } else if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { - const widgetHeight = domNode.domNode.clientHeight; - domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); + const maxRight = (2 * this._verticalScrollbarWidth) + this._minimapWidth; + if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER || widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + const widgetHeight = domNode.domNode.clientHeight; + domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); + } else { + domNode.setTop(0); + } + + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[widgetData.preference]); + stackCoordinates[widgetData.preference] += domNode.domNode.clientWidth; + } else { + domNode.setRight(maxRight); + } } else if (widgetData.preference === OverlayWidgetPositionPreference.TOP_CENTER) { - domNode.setTop(0); domNode.domNode.style.right = '50%'; + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER]); + stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER] += domNode.domNode.clientHeight; + } else { + domNode.setTop(0); + } } else { const { top, left } = widgetData.preference; const fixedOverflowWidgets = this._context.configuration.options.get(EditorOption.fixedOverflowWidgets); @@ -194,9 +211,12 @@ export class ViewOverlayWidgets extends ViewPart { this._domNode.setWidth(this._editorWidth); const keys = Object.keys(this._widgets); + const stackCoordinates = Array.from({ length: OverlayWidgetPositionPreference.TOP_CENTER + 1 }, () => 0); + keys.sort((a, b) => (this._widgets[a].stack || 0) - (this._widgets[b].stack || 0)); + for (let i = 0, len = keys.length; i < len; i++) { const widgetId = keys[i]; - this._renderWidget(this._widgets[widgetId]); + this._renderWidget(this._widgets[widgetId], stackCoordinates); } } } diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 69bf235e7cb..07753688f7e 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -289,7 +289,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._register(new EditorContextKeysManager(this, this._contextKeyService)); this._register(new EditorModeContext(this, this._contextKeyService, languageFeaturesService)); - this._instantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this._contextKeyService])); + this._instantiationService = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, this._contextKeyService]))); this._modelData = null; diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index fbaa5ffcabb..cd5c2e8b606 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -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, bindContextKey, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; @@ -67,9 +68,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { public get onDidContentSizeChange() { return this._editors.onDidContentSizeChange; } private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._domElement)); - private readonly _instantiationService = this._parentInstantiationService.createChild( + private readonly _instantiationService = this._register(this._parentInstantiationService.createChild( new ServiceCollection([IContextKeyService, this._contextKeyService]) - ); + )); private readonly _rootSizeObserver: ObservableElementSizeObserver; diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 65705cb8097..3b968353291 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -8,7 +8,7 @@ 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, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableSignalFromEvent, 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'; @@ -16,8 +16,6 @@ import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { TextLength } from 'vs/editor/common/core/textLength'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export function joinCombine(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] { if (arr1.length === 0) { @@ -89,17 +87,6 @@ export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) }); } -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} - export class ObservableElementSizeObserver extends Disposable { private readonly elementSizeObserver: ElementSizeObserver; @@ -440,13 +427,6 @@ function lengthBetweenPositions(position1: Position, position2: Position): TextL } } -export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { - const boundKey = key.bindTo(service); - return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { - boundKey.set(computeValue(reader)); - }); -} - export function filterWithPrevious(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] { let prev: T | undefined; return arr.filter(cur => { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index dab3bff9092..c29fc74bddd 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -30,9 +30,10 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { DiffEditorItemTemplate, TemplateData } from './diffEditorItemTemplate'; import { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from './multiDiffEditorViewModel'; import { ObjectPool } from './objectPool'; +import { localize } from 'vs/nls'; export class MultiDiffEditorWidgetImpl extends Disposable { - private readonly _elements = h('div.monaco-component.multiDiffEditor', [ + private readonly _scrollableElements = h('div.scrollContent', [ h('div@content', { style: { overflow: 'hidden', @@ -42,31 +43,36 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }), ]); - private readonly _sizeObserver = this._register(new ObservableElementSizeObserver(this._element, undefined)); - - private readonly _objectPool = this._register(new ObjectPool((data) => { - const template = this._instantiationService.createInstance( - DiffEditorItemTemplate, - this._elements.content, - this._elements.overflowWidgetsDomNode, - this._workbenchUIElementFactory - ); - template.setData(data); - return template; - })); - private readonly _scrollable = this._register(new Scrollable({ forceIntegerValues: false, scheduleAtNextAnimationFrame: (cb) => scheduleAtNextAnimationFrame(getWindow(this._element), cb), smoothScrollDuration: 100, })); - private readonly _scrollableElement = this._register(new SmoothScrollableElement(this._elements.root, { + private readonly _scrollableElement = this._register(new SmoothScrollableElement(this._scrollableElements.root, { vertical: ScrollbarVisibility.Auto, horizontal: ScrollbarVisibility.Auto, useShadows: false, }, this._scrollable)); + private readonly _elements = h('div.monaco-component.multiDiffEditor', {}, [ + h('div', {}, [this._scrollableElement.getDomNode()]), + h('div.placeholder@placeholder', {}, [h('div', [localize('noChangedFiles', 'No Changed Files') as any])]), + ]); + + private readonly _sizeObserver = this._register(new ObservableElementSizeObserver(this._element, undefined)); + + private readonly _objectPool = this._register(new ObjectPool((data) => { + const template = this._instantiationService.createInstance( + DiffEditorItemTemplate, + this._scrollableElements.content, + this._scrollableElements.overflowWidgetsDomNode, + this._workbenchUIElementFactory + ); + template.setData(data); + 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); @@ -108,9 +114,9 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }); private readonly _contextKeyService = this._register(this._parentContextKeyService.createScoped(this._element)); - private readonly _instantiationService = this._parentInstantiationService.createChild( + private readonly _instantiationService = this._register(this._parentInstantiationService.createChild( new ServiceCollection([IContextKeyService, this._contextKeyService]) - ); + )); constructor( private readonly _element: HTMLElement, @@ -150,14 +156,20 @@ export class MultiDiffEditorWidgetImpl extends Disposable { this._sizeObserver.observe(dimension); })); - this._elements.content.style.position = 'relative'; + this._register(autorun((reader) => { + /** @description Update widget dimension */ + const items = this._viewItems.read(reader); + this._elements.placeholder.classList.toggle('visible', items.length === 0); + })); + + this._scrollableElements.content.style.position = 'relative'; this._register(autorun((reader) => { /** @description Update scroll dimensions */ const height = this._sizeObserver.height.read(reader); - this._elements.root.style.height = `${height}px`; + this._scrollableElements.root.style.height = `${height}px`; const totalHeight = this._totalHeight.read(reader); - this._elements.content.style.height = `${totalHeight}px`; + this._scrollableElements.content.style.height = `${totalHeight}px`; const width = this._sizeObserver.width.read(reader); @@ -177,7 +189,7 @@ export class MultiDiffEditorWidgetImpl extends Disposable { }); })); - _element.replaceChildren(this._scrollableElement.getDomNode()); + _element.replaceChildren(this._elements.root); this._register(toDisposable(() => { _element.replaceChildren(); })); @@ -299,7 +311,7 @@ export class MultiDiffEditorWidgetImpl extends Disposable { itemContentHeightSumBefore += itemContentHeight + this._spaceBetweenPx; } - this._elements.content.style.transform = `translateY(${-(scrollTop + contentScrollOffsetToScrollOffset)}px)`; + this._scrollableElements.content.style.transform = `translateY(${-(scrollTop + contentScrollOffsetToScrollOffset)}px)`; } } diff --git a/src/vs/editor/browser/widget/multiDiffEditor/style.css b/src/vs/editor/browser/widget/multiDiffEditor/style.css index 767de682b59..fc9c877bf78 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/style.css +++ b/src/vs/editor/browser/widget/multiDiffEditor/style.css @@ -5,8 +5,35 @@ .monaco-component.multiDiffEditor { background: var(--vscode-multiDiffEditor-background); + + position: relative; + + height: 100%; + width: 100%; + overflow-y: hidden; + > div { + position: absolute; + top: 0px; + left: 0px; + + height: 100%; + width: 100%; + + &.placeholder { + visibility: hidden; + + &.visible { + visibility: visible; + } + + display: grid; + place-items: center; + place-content: center; + } + } + .active { --vscode-multiDiffEditor-border: var(--vscode-focusBorder); } diff --git a/src/vs/editor/common/core/lineRange.ts b/src/vs/editor/common/core/lineRange.ts index cd5610f41e8..1a7a0d6b527 100644 --- a/src/vs/editor/common/core/lineRange.ts +++ b/src/vs/editor/common/core/lineRange.ts @@ -7,7 +7,6 @@ import { BugIndicatingError } from 'vs/base/common/errors'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Range } from 'vs/editor/common/core/range'; import { findFirstIdxMonotonousOrArrLen, findLastIdxMonotonous, findLastMonotonous } from 'vs/base/common/arraysFind'; -import { ITextModel } from 'vs/editor/common/model'; /** * A range of lines (1-based). @@ -77,32 +76,6 @@ export class LineRange { return new LineRange(lineRange[0], lineRange[1]); } - /** - * @internal - */ - public static invert(range: LineRange, model: ITextModel): LineRange[] { - if (range.isEmpty) { - return []; - } - const result: LineRange[] = []; - if (range.startLineNumber > 1) { - result.push(new LineRange(1, range.startLineNumber)); - } - if (range.endLineNumberExclusive < model.getLineCount() + 1) { - result.push(new LineRange(range.endLineNumberExclusive, model.getLineCount() + 1)); - } - return result.filter(r => !r.isEmpty); - } - - /** - * @internal - */ - public static asRange(lineRange: LineRange, model: ITextModel): Range { - return lineRange.isEmpty - ? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, model.getLineLength(lineRange.startLineNumber)) - : new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, model.getLineLength(lineRange.endLineNumberExclusive - 1)); - } - /** * The start line number. */ diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 7977eca56b4..68fbf632fb4 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -202,9 +202,9 @@ export interface HoverContext { export interface HoverVerbosityRequest { /** - * Whether to increase or decrease the hover's verbosity + * The delta by which to increase/decrease the hover verbosity level */ - action: HoverVerbosityAction; + verbosityDelta: number; /** * The previous hover for the same position */ @@ -834,6 +834,8 @@ export interface CodeActionProvider { displayName?: string; + extensionId?: string; + /** * Provide commands for the given document and range. */ @@ -1877,6 +1879,13 @@ export interface CommentThread { isTemplate: boolean; } +/** + * @internal + */ +export interface AddedCommentThread extends CommentThread { + editorId?: string; +} + /** * @internal */ @@ -1971,7 +1980,7 @@ export interface CommentThreadChangedEvent { /** * Added comment threads. */ - readonly added: CommentThread[]; + readonly added: AddedCommentThread[]; /** * Removed comment threads. diff --git a/src/vs/editor/common/services/findSectionHeaders.ts b/src/vs/editor/common/services/findSectionHeaders.ts index 08bd3709741..8e03723d0dd 100644 --- a/src/vs/editor/common/services/findSectionHeaders.ts +++ b/src/vs/editor/common/services/findSectionHeaders.ts @@ -36,7 +36,7 @@ export interface SectionHeader { shouldBeInComments: boolean; } -const markRegex = /\bMARK:\s*(.*)$/d; +const markRegex = new RegExp('\\bMARK:\\s*(.*)$', 'd'); const trimDashesRegex = /^-+|-+$/g; /** diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 1bcfecfeb97..81ffaa07895 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -18,19 +18,14 @@ export namespace AccessibilityHelpNLS { export const auto_off = nls.localize("auto_off", "The application is configured to never be optimized for usage with a Screen Reader."); export const screenReaderModeEnabled = nls.localize("screenReaderModeEnabled", "Screen Reader Optimized Mode enabled."); export const screenReaderModeDisabled = nls.localize("screenReaderModeDisabled", "Screen Reader Optimized Mode disabled."); - export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior {0}."); - export const tabFocusModeOnMsgNoKb = nls.localize("tabFocusModeOnMsgNoKb", "Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding."); - export const stickScrollKb = nls.localize("stickScrollKb", "Focus Sticky Scroll ({0}) to focus the currently nested scopes."); - export const stickScrollNoKb = nls.localize("stickScrollNoKb", "Focus Sticky Scroll to focus the currently nested scopes. It is currently not triggerable by a keybinding."); - export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior {0}."); - export const tabFocusModeOffMsgNoKb = nls.localize("tabFocusModeOffMsgNoKb", "Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding."); + export const tabFocusModeOnMsg = nls.localize("tabFocusModeOnMsg", "Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior"); + export const tabFocusModeOffMsg = nls.localize("tabFocusModeOffMsg", "Pressing Tab in the current editor will insert the tab character. Toggle this behavior"); + export const stickScroll = nls.localize("stickScrollKb", "Focus Sticky Scroll to focus the currently nested scopes."); export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help"); export const listSignalSounds = nls.localize("listSignalSoundsCommand", "Run the command: List Signal Sounds for an overview of all sounds and their current status."); export const listAlerts = nls.localize("listAnnouncementsCommand", "Run the command: List Signal Announcements for an overview of announcements and their current status."); - export const quickChat = nls.localize("quickChatCommand", "Toggle quick chat ({0}) to open or close a chat session."); - export const quickChatNoKb = nls.localize("quickChatCommandNoKb", "Toggle quick chat is not currently triggerable by a keybinding."); - export const startInlineChat = nls.localize("startInlineChatCommand", "Start inline chat ({0}) to create an in editor chat session."); - export const startInlineChatNoKb = nls.localize("startInlineChatCommandNoKb", "The command: Start inline chat is not currentlyt riggerable by a keybinding."); + export const quickChat = nls.localize("quickChatCommand", "Toggle quick chat to open or close a chat session.",); + export const startInlineChat = nls.localize("startInlineChatCommand", "Start inline chat to create an in editor chat session."); } export namespace InspectTokensNLS { diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index b18e3dd0b81..e10b43e1f37 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -111,7 +111,6 @@ export const CopyAction = supportsCopy ? registerCommand(new MultiCommand({ MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { submenu: MenuId.MenubarCopy, title: nls.localize2('copy as', "Copy As"), group: '2_ccp', order: 3 }); MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextCopy, title: nls.localize2('copy as', "Copy As"), group: CLIPBOARD_CONTEXT_MENU_GROUP, order: 3 }); MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1, when: ContextKeyExpr.and(ContextKeyExpr.notEquals('resourceScheme', 'output'), EditorContextKeys.editorTextFocus) }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { submenu: MenuId.EditorTitleContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1 }); MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { submenu: MenuId.ExplorerContextShare, title: nls.localize2('share', "Share"), group: '11_share', order: -1 }); export const PasteAction = supportsPaste ? registerCommand(new MultiCommand({ diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 2d7972c19ee..65dc839da2c 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -317,11 +317,13 @@ export class CodeActionController extends Disposable implements IEditorContribut 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'; }; @@ -329,6 +331,7 @@ export class CodeActionController extends Disposable implements IEditorContribut this._telemetryService.publicLog2('codeAction.showCodeActionList.onHide', { codeActionListLength: actions.validActions.length, didCancel: didCancel, + codeActions: actions.validActions.map(action => action.action.title), }); } }, diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 3e62c4c0281..94518efd415 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -7,7 +7,6 @@ import * as dom from 'vs/base/browser/dom'; import { Gesture } from 'vs/base/browser/touch'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; -import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; import { Disposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./lightBulbWidget'; @@ -16,11 +15,10 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition } from 'vs/editor/common/core/position'; import { computeIndentLevel } from 'vs/editor/common/model/utils'; import { autoFixCommandId, quickFixCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { CodeActionKind, CodeActionSet, CodeActionTrigger } from 'vs/editor/contrib/codeAction/common/types'; +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'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; namespace LightBulbState { @@ -65,8 +63,7 @@ export class LightBulbWidget extends Disposable implements IContentWidget { constructor( private readonly _editor: ICodeEditor, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ICommandService commandService: ICommandService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ICommandService commandService: ICommandService ) { super(); @@ -200,24 +197,6 @@ export class LightBulbWidget extends Disposable implements IContentWidget { return; } - const hierarchicalKind = new HierarchicalKind(actionKind); - - if (CodeActionKind.RefactorMove.contains(hierarchicalKind)) { - // Telemetry for showing code actions here. only log on `showLightbulb`. Logs when code action list is quit out. - type ShowCodeActionListEvent = { - codeActionListLength: number; - }; - - 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.' }; - owner: 'justschen'; - comment: 'Event used to gain insights into how often the lightbulb only contains one code action, namely the move to code action. '; - }; - - this._telemetryService.publicLog2('lightbulbWidget.moveToCodeActions', { - codeActionListLength: validActions.length, - }); - } this._editor.layoutContentWidget(this); } diff --git a/src/vs/editor/contrib/codelens/browser/codelensController.ts b/src/vs/editor/contrib/codelens/browser/codelensController.ts index 2cfcb40b456..494336475f3 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -229,7 +229,7 @@ export class CodeLensContribution implements IEditorContribution { this._resolveCodeLensesPromise?.cancel(); this._resolveCodeLensesPromise = undefined; })); - this._localToDispose.add(this._editor.onDidFocusEditorWidget(() => { + this._localToDispose.add(this._editor.onDidFocusEditorText(() => { scheduler.schedule(); })); this._localToDispose.add(this._editor.onDidBlurEditorText(() => { diff --git a/src/vs/editor/contrib/format/browser/format.ts b/src/vs/editor/contrib/format/browser/format.ts index dbf6ede9eae..bb449bb7934 100644 --- a/src/vs/editor/contrib/format/browser/format.ts +++ b/src/vs/editor/contrib/format/browser/format.ts @@ -414,6 +414,23 @@ export async function getDocumentFormattingEditsUntilResult( return undefined; } +export async function getDocumentFormattingEditsWithSelectedProvider( + workerService: IEditorWorkerService, + languageFeaturesService: ILanguageFeaturesService, + editorOrModel: ITextModel | IActiveCodeEditor, + mode: FormattingMode, + token: CancellationToken, +): Promise { + const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel; + const provider = getRealAndSyntheticDocumentFormattersOrdered(languageFeaturesService.documentFormattingEditProvider, languageFeaturesService.documentRangeFormattingEditProvider, model); + const selected = await FormattingConflicts.select(provider, model, mode, FormattingKind.File); + if (selected) { + const rawEdits = await Promise.resolve(selected.provideDocumentFormattingEdits(model, model.getOptions(), token)).catch(onUnexpectedExternalError); + return await workerService.computeMoreMinimalEdits(model.uri, rawEdits); + } + return undefined; +} + export function getOnTypeFormattingEdits( workerService: IEditorWorkerService, languageFeaturesService: ILanguageFeaturesService, diff --git a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts new file mode 100644 index 00000000000..557f1fffa85 --- /dev/null +++ b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { AccessibleViewType, AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +export class HoverAccessibleView implements IAccessibleViewImplentation { + readonly type = AccessibleViewType.View; + readonly priority = 95; + readonly name = 'hover'; + readonly when = EditorContextKeys.hoverFocused; + getProvider(accessor: ServicesAccessor) { + const codeEditorService = accessor.get(ICodeEditorService); + const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + const editorHoverContent = editor ? HoverController.get(editor)?.getWidgetContent() ?? undefined : undefined; + if (!editor || !editorHoverContent) { + return; + } + return { + id: AccessibleViewProviderId.Hover, + verbositySettingKey: 'accessibility.verbosity.hover', + provideContent() { return editorHoverContent; }, + onClose() { + HoverController.get(editor)?.focus(); + }, + options: { + language: editor?.getModel()?.getLanguageId() ?? 'typescript', + type: AccessibleViewType.View + } + }; + } +} + +export class ExtHoverAccessibleView implements IAccessibleViewImplentation { + readonly type = AccessibleViewType.View; + readonly priority = 90; + readonly name = 'extension-hover'; + getProvider(accessor: ServicesAccessor) { + const contextViewService = accessor.get(IContextViewService); + const contextViewElement = contextViewService.getContextViewElement(); + const extensionHoverContent = contextViewElement?.textContent ?? undefined; + const hoverService = accessor.get(IHoverService); + + if (contextViewElement.classList.contains('accessible-view-container') || !extensionHoverContent) { + // The accessible view, itself, uses the context view service to display the text. We don't want to read that. + return; + } + return { + id: AccessibleViewProviderId.Hover, + verbositySettingKey: 'accessibility.verbosity.hover', + provideContent() { return extensionHoverContent; }, + onClose() { + hoverService.showAndFocusLastHover(); + }, + options: { language: 'typescript', type: AccessibleViewType.View } + }; + } +} diff --git a/src/vs/editor/contrib/hover/browser/hoverContribution.ts b/src/vs/editor/contrib/hover/browser/hoverContribution.ts index 33f5cd8f316..629b189f2db 100644 --- a/src/vs/editor/contrib/hover/browser/hoverContribution.ts +++ b/src/vs/editor/contrib/hover/browser/hoverContribution.ts @@ -12,6 +12,8 @@ import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdo import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/browser/markerHoverParticipant'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; import 'vs/css!./hover'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ExtHoverAccessibleView, HoverAccessibleView } from 'vs/editor/contrib/hover/browser/hoverAccessibleViews'; registerEditorContribution(HoverController.ID, HoverController, EditorContributionInstantiation.BeforeFirstInteraction); registerEditorAction(ShowOrFocusHoverAction); @@ -38,3 +40,5 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-editor .monaco-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); } }); +AccessibleViewRegistry.register(new HoverAccessibleView()); +AccessibleViewRegistry.register(new ExtHoverAccessibleView()); diff --git a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index c6f7896a2c7..3c11bee0849 100644 --- a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { asArray, compareBy, numberComparator } from 'vs/base/common/arrays'; -import { CancellationToken } from 'vs/base/common/cancellation'; +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 { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; @@ -212,6 +212,7 @@ class MarkdownRenderedHoverParts extends Disposable { private _renderedHoverParts: RenderedHoverPart[]; private _hoverFocusInfo: FocusedHoverInfo = { hoverPartIndex: -1, focusRemains: false }; + private _ongoingHoverOperations: Map = new Map(); constructor( hoverParts: MarkdownHover[], // we own! @@ -231,6 +232,9 @@ class MarkdownRenderedHoverParts extends Disposable { renderedHoverPart.disposables.dispose(); }); })); + this._register(toDisposable(() => { + this._ongoingHoverOperations.forEach(operation => { operation.tokenSource.dispose(true); }); + })); } private _renderHoverParts( @@ -351,33 +355,45 @@ class MarkdownRenderedHoverParts extends Disposable { if (!hoverRenderedPart || !hoverRenderedPart.hoverSource?.supportsVerbosityAction(action)) { return; } - const hoverPosition = hoverRenderedPart.hoverSource.hoverPosition; - const hoverProvider = hoverRenderedPart.hoverSource.hoverProvider; - const hover = hoverRenderedPart.hoverSource.hover; - const hoverContext: HoverContext = { verbosityRequest: { action, previousHover: hover } }; - - let newHover: Hover | null | undefined; - try { - newHover = await Promise.resolve(hoverProvider.provideHover(model, hoverPosition, CancellationToken.None, hoverContext)); - } catch (e) { - onUnexpectedExternalError(e); - } + const hoverSource = hoverRenderedPart.hoverSource; + const newHover = await this._fetchHover(hoverSource, model, action); if (!newHover) { return; } - - const hoverSource = new HoverSource(newHover, hoverProvider, hoverPosition); - const renderedHoverPart = this._renderHoverPart( + const newHoverSource = new HoverSource(newHover, hoverSource.hoverProvider, hoverSource.hoverPosition); + const newHoverRenderedPart = this._renderHoverPart( hoverFocusedPartIndex, newHover.contents, - hoverSource, + newHoverSource, this._onFinishedRendering ); - this._replaceRenderedHoverPartAtIndex(hoverFocusedPartIndex, renderedHoverPart); + this._replaceRenderedHoverPartAtIndex(hoverFocusedPartIndex, newHoverRenderedPart); this._focusOnHoverPartWithIndex(hoverFocusedPartIndex); this._onFinishedRendering(); } + private async _fetchHover(hoverSource: HoverSource, model: ITextModel, action: HoverVerbosityAction): Promise { + let verbosityDelta = action === HoverVerbosityAction.Increase ? 1 : -1; + const provider = hoverSource.hoverProvider; + const ongoingHoverOperation = this._ongoingHoverOperations.get(provider); + if (ongoingHoverOperation) { + ongoingHoverOperation.tokenSource.cancel(); + verbosityDelta += ongoingHoverOperation.verbosityDelta; + } + const tokenSource = new CancellationTokenSource(); + this._ongoingHoverOperations.set(provider, { verbosityDelta, tokenSource }); + const context: HoverContext = { verbosityRequest: { verbosityDelta, previousHover: hoverSource.hover } }; + let hover: Hover | null | undefined; + try { + hover = await Promise.resolve(provider.provideHover(model, hoverSource.hoverPosition, tokenSource.token, context)); + } catch (e) { + onUnexpectedExternalError(e); + } + tokenSource.dispose(); + this._ongoingHoverOperations.delete(provider); + return hover; + } + private _replaceRenderedHoverPartAtIndex(index: number, renderedHoverPart: RenderedHoverPart): void { if (index >= this._renderHoverParts.length || index < 0) { return; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts index ff80048cc4d..2819af72061 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts @@ -7,7 +7,9 @@ import { EditorContributionInstantiation, registerEditorAction, registerEditorCo import { HoverParticipantRegistry } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { TriggerInlineSuggestionAction, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, AcceptNextWordOfInlineCompletion, AcceptInlineCompletion, HideInlineCompletion, ToggleAlwaysShowInlineSuggestionToolbar, AcceptNextLineOfInlineCompletion } from 'vs/editor/contrib/inlineCompletions/browser/commands'; import { InlineCompletionsHoverParticipant } from 'vs/editor/contrib/inlineCompletions/browser/hoverParticipant'; +import { InlineCompletionsAccessibleView } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { registerAction2 } from 'vs/platform/actions/common/actions'; registerEditorContribution(InlineCompletionsController.ID, InlineCompletionsController, EditorContributionInstantiation.Eventually); @@ -22,3 +24,5 @@ registerEditorAction(HideInlineCompletion); registerAction2(ToggleAlwaysShowInlineSuggestionToolbar); HoverParticipantRegistry.register(InlineCompletionsHoverParticipant); + +AccessibleViewRegistry.register(new InlineCompletionsAccessibleView()); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts new file mode 100644 index 00000000000..6182681a3b5 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.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 { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; +import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; +import { AccessibleViewType, AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +export class InlineCompletionsAccessibleView extends Disposable implements IAccessibleViewImplentation { + readonly type = AccessibleViewType.View; + readonly priority = 95; + readonly name = 'inline-completions'; + readonly when = ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible); + getProvider(accessor: ServicesAccessor) { + const codeEditorService = accessor.get(ICodeEditorService); + function resolveProvider() { + const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!editor) { + return; + } + const model = InlineCompletionsController.get(editor)?.model.get(); + const state = model?.state.get(); + if (!model || !state) { + return; + } + const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); + const ghostText = state.primaryGhostText.renderForScreenReader(lineText); + if (!ghostText) { + return; + } + const language = editor.getModel()?.getLanguageId() ?? undefined; + return { + id: AccessibleViewProviderId.InlineCompletions, + verbositySettingKey: 'accessibility.verbosity.inlineCompletions', + provideContent() { return lineText + ghostText; }, + onClose() { + model.stop(); + editor.focus(); + }, + next() { + model.next(); + setTimeout(() => resolveProvider(), 50); + }, + previous() { + model.previous(); + setTimeout(() => resolveProvider(), 50); + }, + options: { language, type: AccessibleViewType.View } + }; + } + return resolveProvider(); + } + constructor() { + super(); + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts index 00c79719484..9f7d3d17ba8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget.ts @@ -11,7 +11,8 @@ import { equals } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, autorunWithStore, derived, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, observableFromEvent } from 'vs/base/common/observable'; +import { derivedWithStore } from 'vs/base/common/observableInternal/derived'; import { OS } from 'vs/base/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./inlineCompletionsHintsWidget'; @@ -71,26 +72,36 @@ export class InlineCompletionsHintsWidget extends Disposable { return; } - const contentWidget = store.add(this.instantiationService.createInstance( - InlineSuggestionHintsContentWidget, - this.editor, - true, - this.position, - model.selectedInlineCompletionIndex, - model.inlineCompletionsCount, - model.activeCommands, - )); - editor.addContentWidget(contentWidget); - store.add(toDisposable(() => editor.removeContentWidget(contentWidget))); + const contentWidgetValue = derivedWithStore((reader, store) => { + const contentWidget = store.add(this.instantiationService.createInstance( + InlineSuggestionHintsContentWidget, + this.editor, + true, + this.position, + model.selectedInlineCompletionIndex, + model.inlineCompletionsCount, + model.activeCommands, + )); + editor.addContentWidget(contentWidget); + store.add(toDisposable(() => editor.removeContentWidget(contentWidget))); + store.add(autorun(reader => { + /** @description request explicit */ + const position = this.position.read(reader); + if (!position) { + return; + } + if (model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) { + model.triggerExplicitly(); + } + })); + return contentWidget; + }); + + const hadPosition = derivedObservableWithCache(this, (reader, lastValue) => !!this.position.read(reader) || !!lastValue); store.add(autorun(reader => { - /** @description request explicit */ - const position = this.position.read(reader); - if (!position) { - return; - } - if (model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) { - model.triggerExplicitly(); + if (hadPosition.read(reader)) { + contentWidgetValue.read(reader); } })); })); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index 8afc9c241cf..3bc52c6c915 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -64,6 +64,7 @@ box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; z-index: 4; background-color: var(--vscode-editorStickyScroll-background); + right: initial !important; } .monaco-editor .sticky-widget.peek { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 7fb46075f2c..b6a46b82ebd 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -9,8 +9,8 @@ import { equals } from 'vs/base/common/arrays'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { disableNonGpuRendering } from 'vs/editor/browser/view/gpu/gpuViewLayer'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { getColumnOfNodeOffset } from 'vs/editor/browser/viewParts/lines/viewLine'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -393,7 +393,8 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { getPosition(): IOverlayWidgetPosition | null { return { - preference: null + preference: OverlayWidgetPositionPreference.TOP_CENTER, + stackOridinal: 10, }; } diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 31ee2f5038b..5e15b129e0e 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -792,6 +792,7 @@ class StandaloneTelemetryService implements ITelemetryService { readonly sessionId = 'someValue.sessionId'; readonly machineId = 'someValue.machineId'; readonly sqmId = 'someValue.sqmId'; + readonly devDeviceId = 'someValue.devDeviceId'; readonly firstSessionDate = 'someValue.firstSessionDate'; readonly sendErrorTelemetry = false; setEnabled(): void { } @@ -1083,6 +1084,10 @@ class StandaloneAccessbilitySignalService implements IAccessibilitySignalService return ValueWithChangeEvent.const(false); } + getDelayMs(signal: AccessibilitySignal, modality: AccessibilityModality): number { + return 0; + } + isSoundEnabled(cue: AccessibilitySignal): boolean { return false; } diff --git a/src/vs/editor/test/common/modes/supports/onEnterRules.ts b/src/vs/editor/test/common/modes/supports/onEnterRules.ts index 94869ad640f..66e683fdcf8 100644 --- a/src/vs/editor/test/common/modes/supports/onEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/onEnterRules.ts @@ -118,14 +118,14 @@ export const cppOnEnterRules = [ export const htmlOnEnterRules = [ { - beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, - afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>/i, + beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w\-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, + afterText: /^<\/([_:\w][_:\w\-.\d]*)\s*>/i, action: { indentAction: IndentAction.IndentOutdent } }, { - beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, + beforeText: /<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\w][_:\w\-.\d]*)(?:(?:[^'"/>]|"[^"]*"|'[^']*')*?(?!\/)>)[^<]*$/i, action: { indentAction: IndentAction.Indent } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e2c5bd2ea0b..7deb56947e1 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5387,12 +5387,21 @@ declare namespace monaco.editor { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: IEvent; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ @@ -6876,9 +6885,9 @@ declare namespace monaco.languages { export interface HoverVerbosityRequest { /** - * Whether to increase or decrease the hover's verbosity + * The delta by which to increase/decrease the hover verbosity level */ - action: HoverVerbosityAction; + verbosityDelta: number; /** * The previous hover for the same position */ diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts new file mode 100644 index 00000000000..d2fedcb8661 --- /dev/null +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { Event } from 'vs/base/common/event'; +import { IAction } from 'vs/base/common/actions'; +import { IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; + +export const IAccessibleViewService = createDecorator('accessibleViewService'); + +export const enum AccessibleViewProviderId { + Terminal = 'terminal', + TerminalChat = 'terminal-chat', + TerminalHelp = 'terminal-help', + DiffEditor = 'diffEditor', + Chat = 'panelChat', + InlineChat = 'inlineChat', + InlineCompletions = 'inlineCompletions', + KeybindingsEditor = 'keybindingsEditor', + Notebook = 'notebook', + Editor = 'editor', + Hover = 'hover', + Notification = 'notification', + EmptyEditorHint = 'emptyEditorHint', + Comments = 'comments' +} + +export const enum AccessibleViewType { + Help = 'help', + View = 'view' +} + +export const enum NavigationType { + Previous = 'previous', + Next = 'next' +} + +export interface IAccessibleViewOptions { + readMoreUrl?: string; + /** + * Defaults to markdown + */ + language?: string; + type: AccessibleViewType; + /** + * By default, places the cursor on the top line of the accessible view. + * If set to 'initial-bottom', places the cursor on the bottom line of the accessible view and preserves it henceforth. + * If set to 'bottom', places the cursor on the bottom line of the accessible view. + */ + position?: 'bottom' | 'initial-bottom'; + /** + * @returns a string that will be used as the content of the help dialog + * instead of the one provided by default. + */ + customHelp?: () => string; + /** + * If this provider might want to request to be shown again, provide an ID. + */ + id?: AccessibleViewProviderId; + + /** + * Keybinding items to configure + */ + configureKeybindingItems?: IQuickPickItem[]; +} + + +export interface IAccessibleViewContentProvider extends IBasicContentProvider { + id: AccessibleViewProviderId; + verbositySettingKey: string; + /** + * Note that a Codicon class should be provided for each action. + * If not, a default will be used. + */ + onKeyDown?(e: IKeyboardEvent): void; + /** + * When the language is markdown, this is provided by default. + */ + getSymbols?(): IAccessibleViewSymbol[]; + /** + * Note that this will only take effect if the provider has an ID. + */ + onDidRequestClearLastProvider?: Event; +} + + +export interface IAccessibleViewSymbol extends IPickerQuickAccessItem { + markdownToParse?: string; + firstListItem?: string; + lineNumber?: number; + endLineNumber?: number; +} + +export interface IPosition { + lineNumber: number; + column: number; +} + +export interface IAccessibleViewService { + readonly _serviceBrand: undefined; + show(provider: AccesibleViewContentProvider, position?: IPosition): void; + showLastProvider(id: AccessibleViewProviderId): void; + showAccessibleViewHelp(): void; + next(): void; + previous(): void; + navigateToCodeBlock(type: 'next' | 'previous'): void; + goToSymbol(): void; + disableHint(): void; + getPosition(id: AccessibleViewProviderId): IPosition | undefined; + setPosition(position: IPosition, reveal?: boolean): void; + getLastPosition(): IPosition | undefined; + /** + * If the setting is enabled, provides the open accessible view hint as a localized string. + * @param verbositySettingKey The setting key for the verbosity of the feature + */ + getOpenAriaHint(verbositySettingKey: string): string | null; + getCodeBlockContext(): ICodeBlockActionContext | undefined; + configureKeybindings(): void; + openHelpLink(): void; +} + + +export interface ICodeBlockActionContext { + code: string; + languageId?: string; + codeBlockIndex: number; + element: unknown; +} + +export type AccesibleViewContentProvider = AdvancedContentProvider | ExtensionContentProvider; + +export class AdvancedContentProvider implements IAccessibleViewContentProvider { + + constructor( + public id: AccessibleViewProviderId, + public options: IAccessibleViewOptions, + public provideContent: () => string, + public onClose: () => void, + public verbositySettingKey: string, + public actions?: IAction[], + public next?: () => void, + public previous?: () => void, + public onKeyDown?: (e: IKeyboardEvent) => void, + public getSymbols?: () => IAccessibleViewSymbol[], + public onDidRequestClearLastProvider?: Event, + ) { } +} + +export class ExtensionContentProvider implements IBasicContentProvider { + + constructor( + public readonly id: string, + public options: IAccessibleViewOptions, + public provideContent: () => string, + public onClose: () => void, + public next?: () => void, + public previous?: () => void, + public actions?: IAction[], + ) { } +} + +export interface IBasicContentProvider { + id: string; + options: IAccessibleViewOptions; + onClose(): void; + provideContent(): string; + actions?: IAction[]; + previous?(): void; + next?(): void; +} diff --git a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts new file mode 100644 index 00000000000..13679e64781 --- /dev/null +++ b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { alert } from 'vs/base/browser/ui/aria/aria'; + +export interface IAccessibleViewImplentation { + type: AccessibleViewType; + priority: number; + name: string; + /** + * @returns the provider or undefined if the view should not be shown + */ + getProvider: (accessor: ServicesAccessor) => AdvancedContentProvider | ExtensionContentProvider | undefined; + when?: ContextKeyExpression | undefined; +} + +export const AccessibleViewRegistry = new class AccessibleViewRegistry { + _implementations: IAccessibleViewImplentation[] = []; + + register(implementation: IAccessibleViewImplentation): IDisposable { + this._implementations.push(implementation); + return { + dispose: () => { + const idx = this._implementations.indexOf(implementation); + if (idx !== -1) { + this._implementations.splice(idx, 1); + } + } + }; + } + + getImplementations(): IAccessibleViewImplentation[] { + return this._implementations; + } +}; + +export function alertAccessibleViewFocusChange(index: number | undefined, length: number | undefined, type: 'next' | 'previous'): void { + if (index === undefined || length === undefined) { + return; + } + const number = index + 1; + + if (type === 'next' && number + 1 <= length) { + alert(`Focused ${number + 1} of ${length}`); + } else if (type === 'previous' && number - 1 > 0) { + alert(`Focused ${number - 1} of ${length}`); + } + return; +} diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index ba277dbc24e..65515c237ad 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -8,12 +8,13 @@ import { getStructuralKey } from 'vs/base/common/equals'; import { Event, IValueWithChangeEvent } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; -import { derived, IObservable, observableFromEvent } from 'vs/base/common/observable'; +import { derived, observableFromEvent } from 'vs/base/common/observable'; import { ValueWithChangeEventFromObservable } from 'vs/base/common/observableInternal/utils'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IAccessibilitySignalService = createDecorator('accessibilitySignalService'); @@ -25,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; /** * 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. @@ -201,7 +202,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi private readonly _signalConfigValue = new CachedFunction((signal: AccessibilitySignal) => observableConfigValue<{ sound: EnabledState; announcement: EnabledState; - }>(signal.settingsKey, this.configurationService)); + }>(signal.settingsKey, { sound: 'off', announcement: 'off' }, this.configurationService)); private readonly _signalEnabledState = new CachedFunction( { getCacheKey: getStructuralKey }, @@ -239,6 +240,12 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi public onSoundEnabledChanged(signal: AccessibilitySignal): Event { 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; + } } type EnabledState = 'on' | 'off' | 'auto' | 'userGesture' | 'always' | 'never'; @@ -279,6 +286,7 @@ export class Sound { public static readonly error = Sound.register({ fileName: 'error.mp3' }); public static readonly warning = Sound.register({ fileName: 'warning.mp3' }); + public static readonly success = Sound.register({ fileName: 'success.mp3' }); public static readonly foldedArea = Sound.register({ fileName: 'foldedAreas.mp3' }); public static readonly break = Sound.register({ fileName: 'break.mp3' }); public static readonly quickFixes = Sound.register({ fileName: 'quickFixes.mp3' }); @@ -326,6 +334,7 @@ export class AccessibilitySignal { public readonly settingsKey: string, public readonly legacyAnnouncementSettingsKey: string | undefined, public readonly announcementMessage: string | undefined, + public readonly delaySettingsKey: string | undefined ) { } private static _signals = new Set(); @@ -342,6 +351,7 @@ export class AccessibilitySignal { settingsKey: string; legacyAnnouncementSettingsKey?: string; announcementMessage?: string; + delaySettingsKey?: string; }): AccessibilitySignal { const soundSource = new SoundSource('randomOneOf' in options.sound ? options.sound.randomOneOf : [options.sound]); const signal = new AccessibilitySignal( @@ -351,6 +361,7 @@ export class AccessibilitySignal { options.settingsKey, options.legacyAnnouncementSettingsKey, options.announcementMessage, + options.delaySettingsKey ); AccessibilitySignal._signals.add(signal); return signal; @@ -365,12 +376,14 @@ export class AccessibilitySignal { sound: Sound.error, announcementMessage: localize('accessibility.signals.positionHasError', 'Error'), settingsKey: 'accessibility.signals.positionHasError', + delaySettingsKey: 'accessibility.signalOptions.delays.errorAtPosition' }); public static readonly warningAtPosition = AccessibilitySignal.register({ name: localize('accessibilitySignals.positionHasWarning.name', 'Warning at Position'), sound: Sound.warning, announcementMessage: localize('accessibility.signals.positionHasWarning', 'Warning'), settingsKey: 'accessibility.signals.positionHasWarning', + delaySettingsKey: 'accessibility.signalOptions.delays.warningAtPosition' }); public static readonly errorOnLine = AccessibilitySignal.register({ @@ -467,6 +480,13 @@ export class AccessibilitySignal { settingsKey: 'accessibility.signals.terminalCommandFailed', }); + public static readonly terminalCommandSucceeded = AccessibilitySignal.register({ + name: localize('accessibilitySignals.terminalCommandSucceeded', 'Terminal Command Succeeded'), + sound: Sound.success, + announcementMessage: localize('accessibility.signals.terminalCommandSucceeded', 'Command Succeeded'), + settingsKey: 'accessibility.signals.terminalCommandSucceeded', + }); + public static readonly terminalBell = AccessibilitySignal.register({ name: localize('accessibilitySignals.terminalBell', 'Terminal Bell'), sound: Sound.terminalBell, @@ -589,13 +609,3 @@ export class AccessibilitySignal { }); } -export function observableConfigValue(key: string, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key), - ); -} diff --git a/src/vs/platform/accessibilitySignal/browser/media/success.mp3 b/src/vs/platform/accessibilitySignal/browser/media/success.mp3 new file mode 100644 index 00000000000..dee1d5061e4 Binary files /dev/null and b/src/vs/platform/accessibilitySignal/browser/media/success.mp3 differ diff --git a/src/vs/platform/action/common/action.ts b/src/vs/platform/action/common/action.ts index 8b67550ed21..c0f4c2dd053 100644 --- a/src/vs/platform/action/common/action.ts +++ b/src/vs/platform/action/common/action.ts @@ -85,6 +85,10 @@ export interface ICommandAction { tooltip?: string | ILocalizedString; icon?: Icon; source?: ICommandActionSource; + /** + * Precondition controls enablement (for example for a menu item, show + * it in grey or for a command, do not allow to invoke it) + */ precondition?: ContextKeyExpression; /** diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index e97213f2f99..fa9bd76ed8a 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -18,6 +18,9 @@ import { IKeybindingRule, KeybindingsRegistry } from 'vs/platform/keybinding/com export interface IMenuItem { command: ICommandAction; alt?: ICommandAction; + /** + * Menu item is hidden if this expression returns false. + */ when?: ContextKeyExpression; group?: 'navigation' | string; order?: number; diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index 666829d56be..bc1230a48ea 100644 --- a/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -155,7 +155,7 @@ export class DialogMainService implements IDialogMainService { if (!fileDialogLock) { this.logService.error('[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration'); - return { canceled: true }; + return { canceled: true, filePath: '' }; } try { diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index f8d2ed05f8c..818021ff0c1 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -129,6 +129,7 @@ export interface NativeParsedArgs { 'inspect'?: string; 'inspect-brk'?: string; 'js-flags'?: string; + 'disable-lcd-text'?: boolean; 'disable-gpu'?: boolean; 'disable-gpu-sandbox'?: boolean; 'nolazy'?: boolean; @@ -140,4 +141,8 @@ export interface NativeParsedArgs { 'vmodule'?: string; 'disable-dev-shm-usage'?: boolean; 'ozone-platform'?: string; + 'enable-tracing'?: string; + 'trace-startup-format'?: string; + 'trace-startup-file'?: string; + 'trace-startup-duration'?: string; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 91b5d5aeaba..26b7d9c6937 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -120,6 +120,7 @@ export const OPTIONS: OptionDescriptions> = { 'inspect-extensions': { type: 'string', allowEmptyValue: true, deprecates: ['debugPluginHost'], args: 'port', cat: 't', description: localize('inspect-extensions', "Allow debugging and profiling of extensions. Check the developer tools for the connection URI.") }, 'inspect-brk-extensions': { type: 'string', allowEmptyValue: true, deprecates: ['debugBrkPluginHost'], args: 'port', cat: 't', description: localize('inspect-brk-extensions', "Allow debugging and profiling of extensions with the extension host being paused after start. Check the developer tools for the connection URI.") }, + 'disable-lcd-text': { type: 'boolean', cat: 't', description: localize('disableLCDText', "Disable LCD font rendering.") }, 'disable-gpu': { type: 'boolean', cat: 't', description: localize('disableGPU', "Disable GPU hardware acceleration.") }, 'disable-chromium-sandbox': { type: 'boolean', cat: 't', description: localize('disableChromiumSandbox', "Use this option only when there is requirement to launch the application as sudo user on Linux or when running as an elevated user in an applocker environment on Windows.") }, 'sandbox': { type: 'boolean' }, @@ -204,6 +205,10 @@ export const OPTIONS: OptionDescriptions> = { 'disable-dev-shm-usage': { type: 'boolean' }, 'profile-temp': { type: 'boolean' }, 'ozone-platform': { type: 'string' }, + 'enable-tracing': { type: 'string' }, + 'trace-startup-format': { type: 'string' }, + 'trace-startup-file': { type: 'string' }, + 'trace-startup-duration': { type: 'string' }, _: { type: 'string[]' } // main arguments }; diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 55a1e34ba60..2ac86759fb9 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -17,7 +17,8 @@ import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, - IProductVersion + IProductVersion, ExtensionGalleryErrorCode, + EXTENSION_INSTALL_SOURCE_CONTEXT } 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'; @@ -36,7 +37,6 @@ export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; readonly operation: InstallOperation; - readonly profileLocation: URI; readonly options: InstallExtensionTaskOptions; readonly verificationStatus?: ExtensionVerificationStatus; run(): Promise; @@ -205,7 +205,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); + this.logService.info('Installing extension:', installExtensionTask.identifier.id, options.profileLocation.toString()); // only cache gallery extensions tasks if (!URI.isUri(extension)) { this.installingExtensions.set(getInstallExtensionTaskKey(extension, options.profileLocation), { task: installExtensionTask, waitingTasks: [] }); @@ -226,7 +226,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const existingInstallExtensionTask = !URI.isUri(extension) ? this.installingExtensions.get(getInstallExtensionTaskKey(extension, installExtensionTaskOptions.profileLocation)) : undefined; if (existingInstallExtensionTask) { - this.logService.info('Extension is already requested to install', existingInstallExtensionTask.task.identifier.id); + this.logService.info('Extension is already requested to install', existingInstallExtensionTask.task.identifier.id, installExtensionTaskOptions.profileLocation.toString()); alreadyRequestedInstallations.push(existingInstallExtensionTask.task.waitUntilTaskIsFinished()); } else { createInstallExtensionTask(manifest, extension, installExtensionTaskOptions, undefined); @@ -250,14 +250,14 @@ export abstract class AbstractExtensionManagementService extends Disposable impl if (existingInstallingExtension) { if (this.canWaitForTask(task, existingInstallingExtension.task)) { const identifier = existingInstallingExtension.task.identifier; - this.logService.info('Waiting for already requested installing extension', identifier.id, task.identifier.id); + this.logService.info('Waiting for already requested installing extension', identifier.id, task.identifier.id, options.profileLocation.toString()); existingInstallingExtension.waitingTasks.push(task); // add promise that waits until the extension is completely installed, ie., onDidInstallExtensions event is triggered for this extension alreadyRequestedInstallations.push( Event.toPromise( Event.filter(this.onDidInstallExtensions, results => results.some(result => areSameExtensions(result.identifier, identifier))) ).then(results => { - this.logService.info('Finished waiting for already requested installing extension', identifier.id, task.identifier.id); + this.logService.info('Finished waiting for already requested installing extension', identifier.id, task.identifier.id, options.profileLocation.toString()); const result = results.find(result => areSameExtensions(result.identifier, identifier)); if (!result?.local) { // Extension failed to install @@ -290,35 +290,41 @@ export abstract class AbstractExtensionManagementService extends Disposable impl // Install extensions in parallel and wait until all extensions are installed / failed await this.joinAllSettled([...installingExtensionsMap.entries()].map(async ([key, { task }]) => { const startTime = new Date().getTime(); + let local: ILocalExtension; try { - const local = await task.run(); - await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None))); - if (!URI.isUri(task.source)) { - const isUpdate = task.operation === InstallOperation.Update; - const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; - reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { - extensionData: getGalleryExtensionTelemetryData(task.source), - verificationStatus: task.verificationStatus, - duration: new Date().getTime() - startTime, - durationSinceUpdate - }); - // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. - if (isWeb && task.operation !== InstallOperation.Update) { - try { - await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); - } catch (error) { /* ignore */ } - } - } - installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: local.isApplicationScoped }); + local = await task.run(); + await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, task.options, CancellationToken.None)), ExtensionManagementErrorCode.PostInstall); } catch (e) { const error = toExtensionManagementError(e); if (!URI.isUri(task.source)) { - reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), error }); + reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + error, + source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] + }); } - installExtensionResultsMap.set(key, { error, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.profileLocation, applicationScoped: task.options.isApplicationScoped }); - this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error)); + installExtensionResultsMap.set(key, { error, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, applicationScoped: task.options.isApplicationScoped }); + this.logService.error('Error while installing the extension', task.identifier.id, getErrorMessage(error), task.options.profileLocation.toString()); throw error; } + if (!URI.isUri(task.source)) { + const isUpdate = task.operation === InstallOperation.Update; + const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; + reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + verificationStatus: task.verificationStatus, + duration: new Date().getTime() - startTime, + durationSinceUpdate, + source: task.options.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] + }); + // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. + if (isWeb && task.operation !== InstallOperation.Update) { + try { + await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); + } catch (error) { /* ignore */ } + } + } + installExtensionResultsMap.set(key, { local, identifier: task.identifier, operation: task.operation, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, applicationScoped: local.isApplicationScoped }); })); if (alreadyRequestedInstallations.length) { @@ -346,7 +352,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } return allDepsOrPacks; }; - const getErrorResult = (task: IInstallExtensionTask) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: task.options.context, profileLocation: task.profileLocation, error }); + const getErrorResult = (task: IInstallExtensionTask) => ({ identifier: task.identifier, operation: InstallOperation.Install, source: task.source, context: task.options.context, profileLocation: task.options.profileLocation, error }); const rollbackTasks: IUninstallExtensionTask[] = []; for (const [key, { task, root }] of installingExtensionsMap) { @@ -356,8 +362,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl installExtensionResultsMap.set(key, getErrorResult(task)); } // If the extension is installed by a root task and the root task is failed, then uninstall the extension - else if (result.local && root && !installExtensionResultsMap.get(`${root.identifier.id.toLowerCase()}-${task.profileLocation.toString()}`)?.local) { - rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.profileLocation })); + else if (result.local && root && !installExtensionResultsMap.get(`${root.identifier.id.toLowerCase()}-${task.options.profileLocation.toString()}`)?.local) { + rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.options.profileLocation })); installExtensionResultsMap.set(key, getErrorResult(task)); } } @@ -369,9 +375,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl if (task.options.donotIncludePackAndDependencies) { continue; } - const depsOrPacks = getAllDepsAndPacks(result.local, task.profileLocation, [result.local.identifier.id.toLowerCase()]).slice(1); - if (depsOrPacks.some(depOrPack => installingExtensionsMap.has(`${depOrPack.toLowerCase()}-${task.profileLocation.toString()}`) && !installExtensionResultsMap.get(`${depOrPack.toLowerCase()}-${task.profileLocation.toString()}`)?.local)) { - rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.profileLocation })); + const depsOrPacks = getAllDepsAndPacks(result.local, task.options.profileLocation, [result.local.identifier.id.toLowerCase()]).slice(1); + if (depsOrPacks.some(depOrPack => installingExtensionsMap.has(`${depOrPack.toLowerCase()}-${task.options.profileLocation.toString()}`) && !installExtensionResultsMap.get(`${depOrPack.toLowerCase()}-${task.options.profileLocation.toString()}`)?.local)) { + rollbackTasks.push(this.createUninstallExtensionTask(result.local, { versionOnly: true, profileLocation: task.options.profileLocation })); installExtensionResultsMap.set(key, getErrorResult(task)); } } @@ -392,14 +398,14 @@ export abstract class AbstractExtensionManagementService extends Disposable impl // Finally, remove all the tasks from the cache for (const { task } of installingExtensionsMap.values()) { if (task.source && !URI.isUri(task.source)) { - this.installingExtensions.delete(getInstallExtensionTaskKey(task.source, task.profileLocation)); + this.installingExtensions.delete(getInstallExtensionTaskKey(task.source, task.options.profileLocation)); } } if (installExtensionResultsMap.size) { const results = [...installExtensionResultsMap.values()]; for (const result of results) { if (result.local) { - this.logService.info(`Extension installed successfully:`, result.identifier.id); + this.logService.info(`Extension installed successfully:`, result.identifier.id, result.profileLocation.toString()); } } this._onDidInstallExtensions.fire(results); @@ -428,36 +434,35 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return true; } - private async joinAllSettled(promises: Promise[]): Promise { + private async joinAllSettled(promises: Promise[], errorCode?: ExtensionManagementErrorCode): Promise { const results: T[] = []; - const errors: any[] = []; + const errors: ExtensionManagementError[] = []; const promiseResults = await Promise.allSettled(promises); for (const r of promiseResults) { if (r.status === 'fulfilled') { results.push(r.value); } else { - errors.push(r.reason); + errors.push(toExtensionManagementError(r.reason, errorCode)); } } + if (!errors.length) { + return results; + } + // Throw if there are errors - if (errors.length) { - if (errors.length === 1) { - throw errors[0]; - } - - let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown); - for (const current of errors) { - const code = current instanceof ExtensionManagementError ? current.code : ExtensionManagementErrorCode.Unknown; - error = new ExtensionManagementError( - current.message ? `${current.message}, ${error.message}` : error.message, - code !== ExtensionManagementErrorCode.Unknown && code !== ExtensionManagementErrorCode.Internal ? code : error.code - ); - } - throw error; + if (errors.length === 1) { + throw errors[0]; } - return results; + let error = new ExtensionManagementError('', ExtensionManagementErrorCode.Unknown); + for (const current of errors) { + error = new ExtensionManagementError( + error.message ? `${error.message}, ${current.message}` : current.message, + current.code !== ExtensionManagementErrorCode.Unknown && current.code !== ExtensionManagementErrorCode.Internal ? current.code : error.code + ); + } + throw error; } private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { @@ -787,21 +792,37 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected abstract copyExtension(extension: ILocalExtension, fromProfileLocation: URI, toProfileLocation: URI, metadata?: Partial): Promise; } -export function toExtensionManagementError(error: Error): ExtensionManagementError { +export function toExtensionManagementError(error: Error, code?: ExtensionManagementErrorCode): ExtensionManagementError { if (error instanceof ExtensionManagementError) { return error; } + let extensionManagementError: ExtensionManagementError; if (error instanceof ExtensionGalleryError) { - const e = new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Gallery); - e.stack = error.stack; - return e; + extensionManagementError = new ExtensionManagementError(error.message, error.code === ExtensionGalleryErrorCode.DownloadFailedWriting ? ExtensionManagementErrorCode.DownloadFailedWriting : ExtensionManagementErrorCode.Gallery); + } else { + extensionManagementError = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : (code ?? ExtensionManagementErrorCode.Internal)); } - const e = new ExtensionManagementError(error.message, isCancellationError(error) ? ExtensionManagementErrorCode.Cancelled : ExtensionManagementErrorCode.Internal); - e.stack = error.stack; - return e; + extensionManagementError.stack = error.stack; + return extensionManagementError; } -function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, verificationStatus, duration, error, durationSinceUpdate }: { extensionData: any; verificationStatus?: ExtensionVerificationStatus; duration?: number; durationSinceUpdate?: number; error?: ExtensionManagementError | ExtensionGalleryError }): void { +function reportTelemetry(telemetryService: ITelemetryService, eventName: string, + { + extensionData, + verificationStatus, + duration, + error, + source, + durationSinceUpdate + }: { + extensionData: any; + verificationStatus?: + ExtensionVerificationStatus; + duration?: number; + durationSinceUpdate?: number; + source?: string; + error?: ExtensionManagementError | ExtensionGalleryError; + }): void { let errorcode: string | undefined; let errorcodeDetail: string | undefined; @@ -834,6 +855,7 @@ function reportTelemetry(telemetryService: ITelemetryService, eventName: string, "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "source": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] @@ -858,12 +880,13 @@ function reportTelemetry(telemetryService: ITelemetryService, eventName: string, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "verificationStatus" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "source": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] } */ - telemetryService.publicLog(eventName, { ...extensionData, verificationStatus, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate }); + telemetryService.publicLog(eventName, { ...extensionData, verificationStatus, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate, source }); } export abstract class AbstractExtensionTask { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index ebdb1bb50b8..cc587b5770e 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1029,16 +1029,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#download', extension.identifier.id); const data = getGalleryExtensionTelemetryData(extension); const startTime = new Date().getTime(); - /* __GDPR__ - "galleryService:downloadVSIX" : { - "owner": "sandy081", - "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - const log = (duration: number) => this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration }); const operationParam = operation === InstallOperation.Install ? 'install' : operation === InstallOperation.Update ? 'update' : ''; const downloadAsset = operationParam ? { @@ -1048,8 +1038,29 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const headers: IHeaders | undefined = extension.queryContext?.[ACTIVITY_HEADER_NAME] ? { [ACTIVITY_HEADER_NAME]: extension.queryContext[ACTIVITY_HEADER_NAME] } : undefined; const context = await this.getAsset(extension.identifier.id, downloadAsset, AssetType.VSIX, headers ? { headers } : undefined); - await this.fileService.writeFile(location, context.stream); - log(new Date().getTime() - startTime); + + try { + await this.fileService.writeFile(location, context.stream); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + /* ignore */ + this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); + } + throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); + } + + /* __GDPR__ + "galleryService:downloadVSIX" : { + "owner": "sandy081", + "duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "${include}": [ + "${GalleryExtensionTelemetryData}" + ] + } + */ + this.telemetryService.publicLog('galleryService:downloadVSIX', { ...data, duration: new Date().getTime() - startTime }); } async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise { @@ -1060,7 +1071,18 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id); const context = await this.getAsset(extension.identifier.id, extension.assets.signature, AssetType.Signature); - await this.fileService.writeFile(location, context.stream); + try { + await this.fileService.writeFile(location, context.stream); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + /* ignore */ + this.logService.warn(`Error while deleting the file ${location.toString()}`, getErrorMessage(e)); + } + throw new ExtensionGalleryError(getErrorMessage(error), ExtensionGalleryErrorCode.DownloadFailedWriting); + } + } async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index d69233901b8..183f2582871 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -17,8 +17,14 @@ export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); export const WEB_EXTENSION_TAG = '__web_extension'; export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; -export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; +export const EXTENSION_INSTALL_SOURCE_CONTEXT = 'extensionInstallSource'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; +export const EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT = 'clientTargetPlatform'; + +export const enum ExtensionInstallSource { + COMMAND = 'command', + SETTINGS_SYNC = 'settingsSync', +} export interface IProductVersion { readonly version: string; @@ -407,7 +413,21 @@ export interface DidUninstallExtensionEvent { readonly workspaceScoped?: boolean; } -export enum ExtensionManagementErrorCode { +export const enum ExtensionGalleryErrorCode { + Timeout = 'Timeout', + Cancelled = 'Cancelled', + Failed = 'Failed', + DownloadFailedWriting = 'DownloadFailedWriting', +} + +export class ExtensionGalleryError extends Error { + constructor(message: string, readonly code: ExtensionGalleryErrorCode) { + super(message); + this.name = code; + } +} + +export const enum ExtensionManagementErrorCode { Unsupported = 'Unsupported', Deprecated = 'Deprecated', Malicious = 'Malicious', @@ -417,11 +437,19 @@ export enum ExtensionManagementErrorCode { Invalid = 'Invalid', Download = 'Download', DownloadSignature = 'DownloadSignature', + DownloadFailedWriting = ExtensionGalleryErrorCode.DownloadFailedWriting, UpdateMetadata = 'UpdateMetadata', Extract = 'Extract', Scanning = 'Scanning', + ScanningExtension = 'ScanningExtension', + ReadUninstalled = 'ReadUninstalled', + UnsetUninstalled = 'UnsetUninstalled', Delete = 'Delete', Rename = 'Rename', + IntializeDefaultProfile = 'IntializeDefaultProfile', + AddToProfile = 'AddToProfile', + InstalledExtensionNotFound = 'InstalledExtensionNotFound', + PostInstall = 'PostInstall', CorruptZip = 'CorruptZip', IncompleteZip = 'IncompleteZip', Signature = 'Signature', @@ -439,19 +467,6 @@ export class ExtensionManagementError extends Error { } } -export enum ExtensionGalleryErrorCode { - Timeout = 'Timeout', - Cancelled = 'Cancelled', - Failed = 'Failed' -} - -export class ExtensionGalleryError extends Error { - constructor(message: string, readonly code: ExtensionGalleryErrorCode) { - super(message); - this.name = code; - } -} - export type InstallOptions = { isBuiltin?: boolean; isWorkspaceScoped?: boolean; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index e7e7bab19e6..301c7b18e70 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -42,7 +42,7 @@ export class ExtensionKey { readonly id: string; constructor( - identifier: IExtensionIdentifier, + readonly identifier: IExtensionIdentifier, readonly version: string, readonly targetPlatform: TargetPlatform = TargetPlatform.UNDEFINED, ) { @@ -120,6 +120,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any "GalleryExtensionTelemetryData" : { "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "name": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "galleryId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "publisherId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "publisherName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -136,6 +137,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension): return { id: new TelemetryTrustedValue(extension.identifier.id), name: new TelemetryTrustedValue(extension.name), + version: extension.version, galleryId: extension.identifier.uuid, publisherId: extension.publisherId, publisherName: extension.publisher, diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 85fb4668e79..0ddae28ed93 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -13,15 +13,29 @@ import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { Promises as FSPromises } from 'vs/base/node/pfs'; -import { CorruptZipMessage } from 'vs/base/node/zip'; +import { buffer, CorruptZipMessage } from 'vs/base/node/zip'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ExtensionVerificationStatus } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { ExtensionVerificationStatus, toExtensionManagementError } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { fromExtractError } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionSignatureVerificationError, ExtensionSignatureVerificationCode, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +type RetryDownloadClassification = { + owner: 'sandy081'; + comment: 'Event reporting the retry of downloading'; + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; + attempts: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of Attempts' }; +}; +type RetryDownloadEvent = { + extensionId: string; + attempts: number; +}; export class ExtensionsDownloader extends Disposable { @@ -37,6 +51,7 @@ export class ExtensionsDownloader extends Disposable { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, ) { super(); @@ -45,28 +60,35 @@ export class ExtensionsDownloader extends Disposable { this.cleanUpPromise = this.cleanUp(); } - async download(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { + async download(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { await this.cleanUpPromise; - const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); - try { - await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); - } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Download); - } + const location = await this.downloadVSIX(extension, operation); let verificationStatus: ExtensionVerificationStatus = false; if (verifySignature && this.shouldVerifySignature(extension)) { - const signatureArchiveLocation = await this.downloadSignatureArchive(extension); + + let signatureArchiveLocation; try { - verificationStatus = await this.extensionSignatureVerificationService.verify(extension.identifier.id, location.fsPath, signatureArchiveLocation.fsPath); + signatureArchiveLocation = await this.downloadSignatureArchive(extension); } catch (error) { - const sigError = error as ExtensionSignatureVerificationError; - verificationStatus = sigError.code; + try { + // Delete the downloaded VSIX if signature archive download fails + await this.delete(location); + } catch (error) { + this.logService.error(error); + } + throw error; + } + + try { + verificationStatus = await this.extensionSignatureVerificationService.verify(extension, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform); + } catch (error) { + verificationStatus = (error as ExtensionSignatureVerificationError).code; if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { try { - // Delete the downloaded vsix before throwing the error + // Delete the downloaded vsix if VSIX or signature archive is invalid await this.delete(location); } catch (error) { this.logService.error(error); @@ -96,16 +118,64 @@ export class ExtensionsDownloader extends Disposable { return isBoolean(value) ? value : true; } - private async downloadSignatureArchive(extension: IGalleryExtension): Promise { - await this.cleanUpPromise; - - const location = joinPath(this.extensionsDownloadDir, `${this.getName(extension)}${ExtensionsDownloader.SignatureArchiveExtension}`); + private async downloadVSIX(extension: IGalleryExtension, operation: InstallOperation): Promise { try { - await this.downloadFile(extension, location, location => this.extensionGalleryService.downloadSignatureArchive(extension, location)); - } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.DownloadSignature); + const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); + const attempts = await this.doDownload(extension, 'vsix', async () => { + await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); + try { + await this.validate(location.fsPath, 'extension/package.json'); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + this.logService.warn(`Error while deleting: ${location.path}`, getErrorMessage(e)); + } + throw error; + } + }, 2); + + if (attempts > 1) { + this.telemetryService.publicLog2('extensiongallery:downloadvsix:retry', { + extensionId: extension.identifier.id, + attempts + }); + } + + return location; + } catch (e) { + throw toExtensionManagementError(e, ExtensionManagementErrorCode.Download); + } + } + + private async downloadSignatureArchive(extension: IGalleryExtension): Promise { + try { + const location = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); + const attempts = await this.doDownload(extension, 'sigzip', async () => { + await this.extensionGalleryService.downloadSignatureArchive(extension, location); + try { + await this.validate(location.fsPath, '.signature.p7s'); + } catch (error) { + try { + await this.fileService.del(location); + } catch (e) { + this.logService.warn(`Error while deleting: ${location.path}`, getErrorMessage(e)); + } + throw error; + } + }, 2); + + if (attempts > 1) { + this.telemetryService.publicLog2('extensiongallery:downloadsigzip:retry', { + extensionId: extension.identifier.id, + attempts + }); + } + + return location; + } catch (e) { + throw toExtensionManagementError(e, ExtensionManagementErrorCode.DownloadSignature); } - return location; } private async downloadFile(extension: IGalleryExtension, location: URI, downloadFn: (location: URI) => Promise): Promise { @@ -122,18 +192,23 @@ export class ExtensionsDownloader extends Disposable { // Download to temporary location first only if file does not exist const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); - if (!await this.fileService.exists(tempLocation)) { + try { await downloadFn(tempLocation); + } catch (error) { + try { + await this.fileService.del(tempLocation); + } catch (e) { /* ignore */ } + throw error; } try { // Rename temp location to original await FSPromises.rename(tempLocation.fsPath, location.fsPath, 2 * 60 * 1000 /* Retry for 2 minutes */); } catch (error) { - try { - await this.fileService.del(tempLocation); - } catch (e) { /* ignore */ } - if (error.code === 'ENOTEMPTY') { + try { await this.fileService.del(tempLocation); } catch (e) { /* ignore */ } + let exists = false; + try { exists = await this.fileService.exists(location); } catch (e) { /* ignore */ } + if (exists) { this.logService.info(`Rename failed because the file was downloaded by another source. So ignoring renaming.`, extension.identifier.id, location.path); } else { this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the file from downloaded location`, tempLocation.path); @@ -142,6 +217,29 @@ export class ExtensionsDownloader extends Disposable { } } + private async doDownload(extension: IGalleryExtension, name: string, downloadFn: () => Promise, retries: number): Promise { + let attempts = 1; + while (true) { + try { + await downloadFn(); + return attempts; + } catch (e) { + if (attempts++ > retries) { + throw e; + } + this.logService.warn(`Failed downloading ${name}. ${getErrorMessage(e)}. Retry again...`, extension.identifier.id); + } + } + } + + protected async validate(zipPath: string, filePath: string): Promise { + try { + await buffer(zipPath, filePath); + } catch (e) { + throw fromExtractError(e); + } + } + async delete(location: URI): Promise { await this.cleanUpPromise; await this.fileService.del(location); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 53630b739fe..38757db3a67 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -8,7 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStringDictionary } from 'vs/base/common/collections'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { getErrorMessage } from 'vs/base/common/errors'; +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'; @@ -17,11 +17,11 @@ import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; -import { isBoolean, isUndefined } from 'vs/base/common/types'; +import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; -import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip'; +import { extract, IFile, zip } from 'vs/base/node/zip'; import * as nls from 'vs/nls'; import { IDownloadService } from 'vs/platform/download/common/download'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -29,14 +29,15 @@ import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVer import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, Metadata, InstallOptions, - IProductVersion + IProductVersion, + EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT, } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; -import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; +import { fromExtractError, getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; import { DidChangeProfileExtensionsEvent, ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher'; import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -49,12 +50,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -interface InstallableExtension { - zipPath: string; - key: ExtensionKey; - metadata: Metadata; -} - export const INativeServerExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface INativeServerExtensionManagementService extends IExtensionManagementService { readonly _serviceBrand: undefined; @@ -63,6 +58,8 @@ export interface INativeServerExtensionManagementService extends IExtensionManag markAsUninstalled(...extensions: IExtension[]): Promise; } +type ExtractExtensionResult = { readonly local: ILocalExtension; readonly verificationStatus?: ExtensionVerificationStatus }; + const DELETED_FOLDER_POSTFIX = '.vsctmp'; export class ExtensionManagementService extends AbstractExtensionManagementService implements INativeServerExtensionManagementService { @@ -71,7 +68,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi private readonly manifestCache: ExtensionsManifestCache; private readonly extensionsDownloader: ExtensionsDownloader; - private readonly installGalleryExtensionsTasks = new Map(); + private readonly extractingGalleryExtensions = new Map>(); constructor( @IExtensionGalleryService galleryService: IExtensionGalleryService, @@ -81,7 +78,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IDownloadService private downloadService: IDownloadService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IFileService private readonly fileService: IFileService, @IProductService productService: IProductService, @IUriIdentityService uriIdentityService: IUriIdentityService, @@ -281,21 +278,84 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } protected createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask { - if (URI.isUri(extension)) { - return new InstallVSIXTask(manifest, extension, options, this.galleryService, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService); - } - - const key = ExtensionKey.create(extension).toString(); - let installExtensionTask = this.installGalleryExtensionsTasks.get(key); - if (!installExtensionTask) { - this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.uriIdentityService, this.userDataProfilesService, this.extensionsScannerService, this.extensionsProfileScannerService, this.logService, this.telemetryService)); - installExtensionTask.waitUntilTaskIsFinished().finally(() => this.installGalleryExtensionsTasks.delete(key)); - } - return installExtensionTask; + const extensionKey = extension instanceof URI ? new ExtensionKey({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, manifest.version) : ExtensionKey.create(extension); + return this.instantiationService.createInstance(InstallExtensionInProfileTask, extensionKey, manifest, extension, options, (operation, token) => { + if (extension instanceof URI) { + return this.extractVSIX(extensionKey, extension, options, token); + } + let promise = this.extractingGalleryExtensions.get(extensionKey.toString()); + if (!promise) { + this.extractingGalleryExtensions.set(extensionKey.toString(), promise = this.downloadAndExtractGalleryExtension(extensionKey, extension, operation, options, token)); + promise.finally(() => this.extractingGalleryExtensions.delete(extensionKey.toString())); + } + return promise; + }, this.extensionsScanner); } protected createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask { - return new UninstallExtensionTask(extension, options.profileLocation, this.extensionsProfileScannerService); + return new UninstallExtensionInProfileTask(extension, options.profileLocation, this.extensionsProfileScannerService); + } + + private async downloadAndExtractGalleryExtension(extensionKey: ExtensionKey, gallery: IGalleryExtension, operation: InstallOperation, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { + const { verificationStatus, location } = await this.extensionsDownloader.download(gallery, operation, !options.donotVerifySignature, options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); + try { + + if (token.isCancellationRequested) { + throw new CancellationError(); + } + + // validate manifest + await getManifest(location.fsPath); + + const local = await this.extensionsScanner.extractUserExtension( + extensionKey, + location.fsPath, + { + id: gallery.identifier.uuid, + publisherId: gallery.publisherId, + publisherDisplayName: gallery.publisherDisplayName, + targetPlatform: gallery.properties.targetPlatform, + isApplicationScoped: options.isApplicationScoped, + isMachineScoped: options.isMachineScoped, + isBuiltin: options.isBuiltin, + isPreReleaseVersion: gallery.properties.isPreReleaseVersion, + hasPreReleaseVersion: gallery.properties.isPreReleaseVersion, + installedTimestamp: Date.now(), + pinned: options.installGivenVersion ? true : !!options.pinned, + preRelease: isBoolean(options.preRelease) + ? options.preRelease + : options.installPreReleaseVersion || gallery.properties.isPreReleaseVersion, + source: 'gallery', + }, + false, + token); + return { local, verificationStatus }; + } catch (error) { + try { + await this.extensionsDownloader.delete(location); + } catch (e) { + /* Ignore */ + this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e)); + } + throw toExtensionManagementError(error); + } + } + + private async extractVSIX(extensionKey: ExtensionKey, location: URI, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { + const local = await this.extensionsScanner.extractUserExtension( + extensionKey, + path.resolve(location.fsPath), + { + isApplicationScoped: options.isApplicationScoped, + isMachineScoped: options.isMachineScoped, + isBuiltin: options.isBuiltin, + installedTimestamp: Date.now(), + pinned: options.installGivenVersion ? true : !!options.pinned, + source: 'vsix', + }, + true, + token); + return { local }; } private async collectFiles(extension: ILocalExtension): Promise { @@ -451,20 +511,28 @@ export class ExtensionsScanner extends Disposable { } async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { - 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)); - } else if (type === ExtensionType.User) { - scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); + try { + 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)); + } else if (type === ExtensionType.User) { + scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(userScanOptions)); + } + scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; + return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); } - scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); } async scanAllUserExtensions(excludeOutdated: boolean): Promise { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + try { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + return await Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); + } } async scanUserExtensionAtLocation(location: URI): Promise { @@ -496,27 +564,35 @@ export class ExtensionsScanner extends Disposable { } if (exists) { - await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); + try { + await this.extensionsScannerService.updateMetadata(extensionLocation, metadata); + } catch (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) { - let errorCode = ExtensionManagementErrorCode.Extract; - if (e instanceof ExtractError) { - if (e.type === 'CorruptZip') { - errorCode = ExtensionManagementErrorCode.CorruptZip; - } else if (e.type === 'Incomplete') { - errorCode = ExtensionManagementErrorCode.IncompleteZip; - } - } - throw new ExtensionManagementError(e.message, errorCode); + throw fromExtractError(e); } - await this.extensionsScannerService.updateMetadata(tempLocation, metadata); + try { + await this.extensionsScannerService.updateMetadata(tempLocation, metadata); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); + } + + if (token.isCancellationRequested) { + throw new CancellationError(); + } // Rename try { @@ -558,16 +634,24 @@ export class ExtensionsScanner extends Disposable { } async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise { - if (profileLocation) { - await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); - } else { - await this.extensionsScannerService.updateMetadata(local.location, metadata); + try { + if (profileLocation) { + await this.extensionsProfileScannerService.updateMetadata([[local, metadata]], profileLocation); + } else { + await this.extensionsScannerService.updateMetadata(local.location, metadata); + } + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UpdateMetadata); } return this.scanLocalExtension(local.location, local.type, profileLocation); } - getUninstalledExtensions(): Promise> { - return this.withUninstalledExtensions(); + async getUninstalledExtensions(): Promise> { + try { + return await this.withUninstalledExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ReadUninstalled); + } } async setUninstalled(...extensions: IExtension[]): Promise { @@ -580,7 +664,11 @@ export class ExtensionsScanner extends Disposable { } async setInstalled(extensionKey: ExtensionKey): Promise { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + try { + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.UnsetUninstalled); + } } async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { @@ -630,7 +718,7 @@ export class ExtensionsScanner extends Disposable { this.logService.info(`Deleted ${type} extension from disk`, id, location.fsPath); } - private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { + private withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; try { @@ -666,24 +754,28 @@ export class ExtensionsScanner extends Disposable { try { await pfs.Promises.rename(extractPath, renamePath, 2 * 60 * 1000 /* Retry for 2 minutes */); } catch (error) { - throw new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); + throw toExtensionManagementError(error, ExtensionManagementErrorCode.Rename); } } private async scanLocalExtension(location: URI, type: ExtensionType, profileLocation?: URI): Promise { - if (profileLocation) { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); - const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); - if (scannedExtension) { - return this.toLocalExtension(scannedExtension); - } - } else { - const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); - if (scannedExtension) { - return this.toLocalExtension(scannedExtension); + try { + if (profileLocation) { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ profileLocation }); + const scannedExtension = scannedExtensions.find(e => this.uriIdentityService.extUri.isEqual(e.location, location)); + if (scannedExtension) { + return await this.toLocalExtension(scannedExtension); + } + } else { + const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); + if (scannedExtension) { + return await this.toLocalExtension(scannedExtension); + } } + throw new ExtensionManagementError(nls.localize('cannot read', "Cannot read the extension from {0}", location.path), ExtensionManagementErrorCode.ScanningExtension); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.ScanningExtension); } - throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path)); } private async toLocalExtension(extension: IScannedExtension): Promise { @@ -791,57 +883,144 @@ export class ExtensionsScanner extends Disposable { } -abstract class InstallExtensionTask extends AbstractExtensionTask implements IInstallExtensionTask { +class InstallExtensionInProfileTask extends AbstractExtensionTask implements IInstallExtensionTask { - private _profileLocation = this.options.profileLocation; - get profileLocation() { return this._profileLocation; } + private _operation = InstallOperation.Install; + get operation() { return this.options.operation ?? this._operation; } - protected _verificationStatus: ExtensionVerificationStatus = false; + private _verificationStatus: ExtensionVerificationStatus | undefined; get verificationStatus() { return this._verificationStatus; } - protected _operation = InstallOperation.Install; - get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; } + readonly identifier: IExtensionIdentifier; constructor( + private readonly extensionKey: ExtensionKey, readonly manifest: IExtensionManifest, - readonly identifier: IExtensionIdentifier, - readonly source: URI | IGalleryExtension, + readonly source: IGalleryExtension | URI, readonly options: InstallExtensionTaskOptions, - protected readonly extensionsScanner: ExtensionsScanner, - protected readonly uriIdentityService: IUriIdentityService, - protected readonly userDataProfilesService: IUserDataProfilesService, - protected readonly extensionsScannerService: IExtensionsScannerService, - protected readonly extensionsProfileScannerService: IExtensionsProfileScannerService, - protected readonly logService: ILogService, + private readonly extractExtensionFn: (operation: InstallOperation, token: CancellationToken) => Promise, + private readonly extensionsScanner: ExtensionsScanner, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, + @ILogService private readonly logService: ILogService, ) { super(); + this.identifier = this.extensionKey.identifier; } - protected override async doRun(token: CancellationToken): Promise { - const [local, metadata] = await this.install(token); - this._profileLocation = local.isBuiltin || local.isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : this.options.profileLocation; - if (this.uriIdentityService.extUri.isEqual(this.userDataProfilesService.defaultProfile.extensionsResource, this._profileLocation)) { - await this.extensionsScannerService.initializeDefaultProfileExtensions(); + private async getExistingExtension(): Promise { + 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(); + if (existingExtension) { + this._operation = InstallOperation.Update; } - await this.extensionsProfileScannerService.addExtensionsToProfile([[local, metadata]], this._profileLocation, !local.isValid); - return local; - } - protected async extractExtension({ zipPath, key, metadata }: InstallableExtension, removeIfExists: boolean, token: CancellationToken): Promise { - let local = await this.unsetIfUninstalled(key); - if (local) { - local = await this.extensionsScanner.updateMetadata(local, metadata); - } else { - this.logService.trace('Extracting extension...', key.id); - local = await this.extensionsScanner.extractUserExtension(key, zipPath, metadata, removeIfExists, token); - this.logService.info('Extracting extension completed.', key.id); + const metadata: Metadata = { + isApplicationScoped: this.options.isApplicationScoped || existingExtension?.isApplicationScoped, + isMachineScoped: this.options.isMachineScoped || existingExtension?.isMachineScoped, + isBuiltin: this.options.isBuiltin || existingExtension?.isBuiltin, + isSystem: existingExtension?.type === ExtensionType.System ? true : undefined, + installedTimestamp: Date.now(), + pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned), + source: this.source instanceof URI ? 'vsix' : 'gallery', + }; + + // VSIX + if (this.source instanceof URI) { + if (existingExtension) { + if (this.extensionKey.equals(new ExtensionKey(existingExtension.identifier, existingExtension.manifest.version))) { + try { + await this.extensionsScanner.removeExtension(existingExtension, 'existing'); + } catch (e) { + throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); + } + } + } else { + // Remove the extension with same version if it is already uninstalled. + // Installing a VSIX extension shall replace the existing extension always. + const existingWithSameVersion = await this.unsetIfUninstalled(this.extensionKey); + if (existingWithSameVersion) { + try { + await this.extensionsScanner.removeExtension(existingWithSameVersion, 'existing'); + } catch (e) { + throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); + } + } + } + } - return local; + + // Gallery + else { + metadata.id = this.source.identifier.uuid; + metadata.publisherId = this.source.publisherId; + metadata.publisherDisplayName = this.source.publisherDisplayName; + metadata.targetPlatform = this.source.properties.targetPlatform; + metadata.updated = !!existingExtension; + metadata.isPreReleaseVersion = this.source.properties.isPreReleaseVersion; + metadata.hasPreReleaseVersion = existingExtension?.hasPreReleaseVersion || this.source.properties.isPreReleaseVersion; + metadata.preRelease = isBoolean(this.options.preRelease) + ? this.options.preRelease + : this.options.installPreReleaseVersion || this.source.properties.isPreReleaseVersion || existingExtension?.preRelease; + + if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.source.version) { + return this.extensionsScanner.updateMetadata(existingExtension, metadata); + } + + // Unset if the extension is uninstalled and return the unset extension. + const local = await this.unsetIfUninstalled(this.extensionKey); + if (local) { + return local; + } + } + + if (token.isCancellationRequested) { + throw toExtensionManagementError(new CancellationError()); + } + + const { local, verificationStatus } = await this.extractExtensionFn(this.operation, token); + this._verificationStatus = verificationStatus; + + if (this.uriIdentityService.extUri.isEqual(this.userDataProfilesService.defaultProfile.extensionsResource, this.options.profileLocation)) { + try { + await this.extensionsScannerService.initializeDefaultProfileExtensions(); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.IntializeDefaultProfile); + } + } + + if (token.isCancellationRequested) { + throw toExtensionManagementError(new CancellationError()); + } + + try { + await this.extensionsProfileScannerService.addExtensionsToProfile([[local, metadata]], this.options.profileLocation, !local.isValid); + } catch (error) { + throw toExtensionManagementError(error, ExtensionManagementErrorCode.AddToProfile); + } + + const result = await this.getExistingExtension(); + if (!result) { + throw new ExtensionManagementError('Cannot find the installed extension', ExtensionManagementErrorCode.InstalledExtensionNotFound); + } + + if (this.source instanceof URI) { + this.updateMetadata(local, token); + } + + return result; } - protected async unsetIfUninstalled(extensionKey: ExtensionKey): Promise { - const isUninstalled = await this.isUninstalled(extensionKey); - if (!isUninstalled) { + private async unsetIfUninstalled(extensionKey: ExtensionKey): Promise { + const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); + if (!uninstalled[extensionKey.toString()]) { return undefined; } @@ -854,202 +1033,6 @@ abstract class InstallExtensionTask extends AbstractExtensionTask ExtensionKey.create(i).equals(extensionKey)); } - private async isUninstalled(extensionId: ExtensionKey): Promise { - const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - return !!uninstalled[extensionId.toString()]; - } - - protected abstract install(token: CancellationToken): Promise<[ILocalExtension, Metadata]>; - -} - -export class InstallGalleryExtensionTask extends InstallExtensionTask { - - constructor( - manifest: IExtensionManifest, - private readonly gallery: IGalleryExtension, - options: InstallExtensionTaskOptions, - private readonly extensionsDownloader: ExtensionsDownloader, - extensionsScanner: ExtensionsScanner, - uriIdentityService: IUriIdentityService, - userDataProfilesService: IUserDataProfilesService, - extensionsScannerService: IExtensionsScannerService, - extensionsProfileScannerService: IExtensionsProfileScannerService, - logService: ILogService, - private readonly telemetryService: ITelemetryService, - ) { - super(manifest, gallery.identifier, gallery, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService); - } - - protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - let installed; - try { - installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); - } catch (error) { - throw new ExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); - } - - const existingExtension = installed.find(i => areSameExtensions(i.identifier, this.gallery.identifier)); - if (existingExtension) { - this._operation = InstallOperation.Update; - } - - const metadata: Metadata = { - id: this.gallery.identifier.uuid, - publisherId: this.gallery.publisherId, - publisherDisplayName: this.gallery.publisherDisplayName, - targetPlatform: this.gallery.properties.targetPlatform, - isApplicationScoped: this.options.isApplicationScoped || existingExtension?.isApplicationScoped, - isMachineScoped: this.options.isMachineScoped || existingExtension?.isMachineScoped, - isBuiltin: this.options.isBuiltin || existingExtension?.isBuiltin, - isSystem: existingExtension?.type === ExtensionType.System ? true : undefined, - updated: !!existingExtension, - isPreReleaseVersion: this.gallery.properties.isPreReleaseVersion, - hasPreReleaseVersion: existingExtension?.hasPreReleaseVersion || this.gallery.properties.isPreReleaseVersion, - installedTimestamp: Date.now(), - pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existingExtension?.pinned), - preRelease: isBoolean(this.options.preRelease) - ? this.options.preRelease - : this.options.installPreReleaseVersion || this.gallery.properties.isPreReleaseVersion || existingExtension?.preRelease, - source: 'gallery', - }; - - if (existingExtension && existingExtension.type !== ExtensionType.System && existingExtension.manifest.version === this.gallery.version) { - try { - const local = await this.extensionsScanner.updateMetadata(existingExtension, metadata); - return [local, metadata]; - } catch (error) { - throw new ExtensionManagementError(getErrorMessage(error), ExtensionManagementErrorCode.UpdateMetadata); - } - } - - try { - return await this.downloadAndInstallExtension(metadata, token); - } catch (error) { - if (error instanceof ExtensionManagementError && (error.code === ExtensionManagementErrorCode.CorruptZip || error.code === ExtensionManagementErrorCode.IncompleteZip)) { - this.logService.info(`Downloaded VSIX is invalid. Trying to download and install again...`, this.gallery.identifier.id); - type RetryInstallingInvalidVSIXClassification = { - owner: 'sandy081'; - comment: 'Event reporting the retry of installing an invalid VSIX'; - extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Extension Id' }; - succeeded?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Success value' }; - }; - type RetryInstallingInvalidVSIXEvent = { - extensionId: string; - succeeded: boolean; - }; - try { - const result = await this.downloadAndInstallExtension(metadata, token); - this.telemetryService.publicLog2('extensiongallery:install:retry', { - extensionId: this.gallery.identifier.id, - succeeded: true - }); - return result; - } catch (error) { - this.telemetryService.publicLog2('extensiongallery:install:retry', { - extensionId: this.gallery.identifier.id, - succeeded: false - }); - throw error; - } - } else { - throw error; - } - } - } - - private async downloadAndInstallExtension(metadata: Metadata, token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - const { location, verificationStatus } = await this.extensionsDownloader.download(this.gallery, this._operation, !this.options.donotVerifySignature); - try { - this._verificationStatus = verificationStatus; - this.validateManifest(location.fsPath); - const local = await this.extractExtension({ zipPath: location.fsPath, key: ExtensionKey.create(this.gallery), metadata }, false, token); - return [local, metadata]; - } catch (error) { - try { - await this.extensionsDownloader.delete(location); - } catch (error) { - /* Ignore */ - this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(error)); - } - throw error; - } - } - - protected async validateManifest(zipPath: string): Promise { - try { - await getManifest(zipPath); - } catch (error) { - throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Invalid); - } - } - -} - -class InstallVSIXTask extends InstallExtensionTask { - - constructor( - manifest: IExtensionManifest, - private readonly location: URI, - options: InstallExtensionTaskOptions, - private readonly galleryService: IExtensionGalleryService, - extensionsScanner: ExtensionsScanner, - uriIdentityService: IUriIdentityService, - userDataProfilesService: IUserDataProfilesService, - extensionsScannerService: IExtensionsScannerService, - extensionsProfileScannerService: IExtensionsProfileScannerService, - logService: ILogService, - ) { - super(manifest, { id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, options, extensionsScanner, uriIdentityService, userDataProfilesService, extensionsScannerService, extensionsProfileScannerService, logService); - } - - protected override async doRun(token: CancellationToken): Promise { - const local = await super.doRun(token); - this.updateMetadata(local, token); - return local; - } - - protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { - const extensionKey = new ExtensionKey(this.identifier, this.manifest.version); - const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion); - const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier)); - const metadata: Metadata = { - isApplicationScoped: this.options.isApplicationScoped || existing?.isApplicationScoped, - isMachineScoped: this.options.isMachineScoped || existing?.isMachineScoped, - isBuiltin: this.options.isBuiltin || existing?.isBuiltin, - installedTimestamp: Date.now(), - pinned: this.options.installGivenVersion ? true : (this.options.pinned ?? existing?.pinned), - source: 'vsix', - }; - - if (existing) { - this._operation = InstallOperation.Update; - if (extensionKey.equals(new ExtensionKey(existing.identifier, existing.manifest.version))) { - try { - await this.extensionsScanner.removeExtension(existing, 'existing'); - } catch (e) { - throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); - } - } else if (!this.options.profileLocation && semver.gt(existing.manifest.version, this.manifest.version)) { - await this.extensionsScanner.setUninstalled(existing); - } - } else { - // Remove the extension with same version if it is already uninstalled. - // Installing a VSIX extension shall replace the existing extension always. - const existing = await this.unsetIfUninstalled(extensionKey); - if (existing) { - try { - await this.extensionsScanner.removeExtension(existing, 'existing'); - } catch (e) { - throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", this.manifest.displayName || this.manifest.name)); - } - } - } - - const local = await this.extractExtension({ zipPath: path.resolve(this.location.fsPath), key: extensionKey, metadata }, true, token); - return [local, metadata]; - } - private async updateMetadata(extension: ILocalExtension, token: CancellationToken): Promise { try { let [galleryExtension] = await this.galleryService.getExtensions([{ id: extension.identifier.id, version: extension.manifest.version }], token); @@ -1073,7 +1056,7 @@ class InstallVSIXTask extends InstallExtensionTask { } } -class UninstallExtensionTask extends AbstractExtensionTask implements IUninstallExtensionTask { +class UninstallExtensionInProfileTask extends AbstractExtensionTask implements IUninstallExtensionTask { constructor( readonly extension: ILocalExtension, @@ -1088,4 +1071,3 @@ class UninstallExtensionTask extends AbstractExtensionTask implements IUni } } - diff --git a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts index 6d9a54272da..96118542408 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementUtil.ts @@ -3,17 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { buffer } from 'vs/base/node/zip'; +import { buffer, ExtractError } from 'vs/base/node/zip'; import { localize } from 'vs/nls'; +import { toExtensionManagementError } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; +import { ExtensionManagementError, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -export function getManifest(vsix: string): Promise { - return buffer(vsix, 'extension/package.json') - .then(buffer => { - try { - return JSON.parse(buffer.toString('utf8')); - } catch (err) { - throw new Error(localize('invalidManifest', "VSIX invalid: package.json is not a JSON file.")); - } - }); +export function fromExtractError(e: Error): ExtensionManagementError { + let errorCode = ExtensionManagementErrorCode.Extract; + if (e instanceof ExtractError) { + if (e.type === 'CorruptZip') { + errorCode = ExtensionManagementErrorCode.CorruptZip; + } else if (e.type === 'Incomplete') { + errorCode = ExtensionManagementErrorCode.IncompleteZip; + } + } + return toExtensionManagementError(e, errorCode); +} + +export async function getManifest(vsixPath: string): Promise { + let data; + try { + data = await buffer(vsixPath, 'extension/package.json'); + } catch (e) { + throw fromExtractError(e); + } + + try { + return JSON.parse(data.toString('utf8')); + } catch (err) { + throw new ExtensionManagementError(localize('invalidManifest', "VSIX invalid: package.json is not a JSON file."), ExtensionManagementErrorCode.Invalid); + } } diff --git a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts index a835fb53111..b50638dbf6d 100644 --- a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts +++ b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { getErrorMessage } from 'vs/base/common/errors'; +import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -26,7 +28,7 @@ export interface IExtensionSignatureVerificationService { * @throws { ExtensionSignatureVerificationError } An error with a code indicating the validity, integrity, or trust issue * found during verification or a more fundamental issue (e.g.: a required dependency was not found). */ - verify(extensionId: string, vsixFilePath: string, signatureArchiveFilePath: string): Promise; + verify(extension: IGalleryExtension, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise; } declare module vsceSign { @@ -70,6 +72,7 @@ export const enum ExtensionSignatureVerificationCode { export interface ExtensionSignatureVerificationResult { readonly code: ExtensionSignatureVerificationCode; readonly didExecute: boolean; + readonly internalCode?: number; readonly output?: string; } @@ -106,8 +109,9 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur return this.moduleLoadingPromise; } - public async verify(extensionId: string, vsixFilePath: string, signatureArchiveFilePath: string): Promise { + public async verify(extension: IGalleryExtension, vsixFilePath: string, signatureArchiveFilePath: string, clientTargetPlatform?: TargetPlatform): Promise { let module: typeof vsceSign; + const extensionId = extension.identifier.id; try { module = await this.vsceSign(); @@ -121,6 +125,7 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur let result: ExtensionSignatureVerificationResult; try { + this.logService.trace(`Verifying extension signature for ${extensionId}...`); result = await module.verify(vsixFilePath, signatureArchiveFilePath, this.logService.getLevel() === LogLevel.Trace); } catch (e) { result = { @@ -132,28 +137,37 @@ export class ExtensionSignatureVerificationService implements IExtensionSignatur const duration = new Date().getTime() - startTime; - this.logService.info(`Extension signature verification result for ${extensionId}: ${result.code}. Duration: ${duration}ms.`); + this.logService.info(`Extension signature verification result for ${extensionId}: ${result.code}. Executed: ${result.didExecute}. Duration: ${duration}ms.`); this.logService.trace(`Extension signature verification output for ${extensionId}:\n${result.output}`); type ExtensionSignatureVerificationClassification = { owner: 'sandy081'; comment: 'Extension signature verification event'; extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension identifier' }; + extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'extension version' }; code: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'result code of the verification' }; + internalCode?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'internal code of the verification' }; duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true; comment: 'amount of time taken to verify the signature' }; didExecute: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'whether the verification was executed' }; + clientTargetPlatform?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'target platform of the client' }; }; type ExtensionSignatureVerificationEvent = { extensionId: string; + extensionVersion: string; code: string; + internalCode?: number; duration: number; didExecute: boolean; + clientTargetPlatform?: string; }; this.telemetryService.publicLog2('extensionsignature:verification', { extensionId, + extensionVersion: extension.version, code: result.code, + internalCode: result.internalCode, duration, - didExecute: result.didExecute + didExecute: result.didExecute, + clientTargetPlatform, }); if (result.code === ExtensionSignatureVerificationCode.Success) { diff --git a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts new file mode 100644 index 00000000000..e803de72c79 --- /dev/null +++ b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { VSBuffer } from 'vs/base/common/buffer'; +import { platform } from 'vs/base/common/platform'; +import { arch } from 'vs/base/common/process'; +import { joinPath } from 'vs/base/common/resources'; +import { isBoolean } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { mock } from 'vs/base/test/common/mock'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; +import { IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; + +const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); + +class TestExtensionSignatureVerificationService extends mock() { + + constructor( + private readonly verificationResult: string | boolean) { + super(); + } + + override async verify(): Promise { + if (isBoolean(this.verificationResult)) { + return this.verificationResult; + } + const error = Error(this.verificationResult); + (error as any).code = this.verificationResult; + throw error; + } +} + +class TestExtensionDownloader extends ExtensionsDownloader { + protected override async validate(): Promise { } +} + +suite('ExtensionDownloader Tests', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + let instantiationService: TestInstantiationService; + + setup(() => { + instantiationService = disposables.add(new TestInstantiationService()); + + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); + + instantiationService.stub(ILogService, logService); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(ILogService, logService); + instantiationService.stub(INativeEnvironmentService, { extensionsDownloadLocation: joinPath(ROOT, 'CachedExtensionVSIXs') }); + instantiationService.stub(IExtensionGalleryService, { + async download(extension, location, operation) { + await fileService.writeFile(location, VSBuffer.fromString('extension vsix')); + }, + async downloadSignatureArchive(extension, location) { + await fileService.writeFile(location, VSBuffer.fromString('extension signature')); + }, + }); + }); + + test('download completes successfully if verification is disabled by setting set to false', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: false, verificationResult: 'error' }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully if verification is disabled by options', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, false); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully if verification is disabled because the module is not loaded', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: false }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully if verification fails to execute', async () => { + const errorCode = 'ENOENT'; + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, errorCode); + }); + + test('download completes successfully if verification fails ', async () => { + const errorCode = 'IntegrityCheckFailed'; + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, errorCode); + }); + + test('download completes successfully if verification succeeds', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, true); + }); + + test('download completes successfully for unsigned extension', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + test('download completes successfully for an unsigned extension even when signature verification throws error', async () => { + const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + + const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); + + assert.strictEqual(actual.verificationStatus, false); + }); + + function aTestObject(options: { isSignatureVerificationEnabled: boolean; verificationResult: boolean | string }): ExtensionsDownloader { + instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); + instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult)); + return disposables.add(instantiationService.createInstance(TestExtensionDownloader)); + } + + function aGalleryExtension(name: string, properties: Partial = {}, galleryExtensionProperties: any = {}, assets: Partial = {}): IGalleryExtension { + const targetPlatform = getTargetPlatform(platform, arch); + const galleryExtension = Object.create({ name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [targetPlatform], properties: {}, assets: {}, ...properties }); + galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], targetPlatform, ...galleryExtensionProperties }; + galleryExtension.assets = { ...galleryExtension.assets, ...assets }; + galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() }; + return galleryExtension; + } +}); diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts deleted file mode 100644 index 92ac41e0cea..00000000000 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ /dev/null @@ -1,248 +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 * as assert from 'assert'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { platform } from 'vs/base/common/platform'; -import { arch } from 'vs/base/common/process'; -import { joinPath } from 'vs/base/common/resources'; -import { isBoolean } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { mock } from 'vs/base/test/common/mock'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; -import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; -import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; -import { ExtensionsScanner, InstallGalleryExtensionTask } from 'vs/platform/extensionManagement/node/extensionManagementService'; -import { IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; -import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService'; -import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; -import { IFileService } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { ILogService, NullLogService } from 'vs/platform/log/common/log'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; -import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; - -const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); - -class TestExtensionsScanner extends mock() { - override async scanExtensions(): Promise { return []; } -} - -class TestExtensionSignatureVerificationService extends mock() { - - constructor( - private readonly verificationResult: string | boolean) { - super(); - } - - override async verify(): Promise { - if (isBoolean(this.verificationResult)) { - return this.verificationResult; - } - const error = Error(this.verificationResult); - (error as any).code = this.verificationResult; - throw error; - } -} - -class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { - - installed = false; - - constructor( - extension: IGalleryExtension, - extensionDownloader: ExtensionsDownloader, - disposables: DisposableStore, - ) { - const instantiationService = disposables.add(new TestInstantiationService()); - const logService = instantiationService.stub(ILogService, new NullLogService()); - const fileService = instantiationService.stub(IFileService, disposables.add(new FileService(logService))); - const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); - const systemExtensionsLocation = joinPath(ROOT, 'system'); - const userExtensionsLocation = joinPath(ROOT, 'extensions'); - instantiationService.stub(INativeEnvironmentService, { - userHome: ROOT, - userRoamingDataHome: ROOT, - builtinExtensionsPath: systemExtensionsLocation.fsPath, - extensionsPath: userExtensionsLocation.fsPath, - userDataPath: userExtensionsLocation.fsPath, - cacheHome: ROOT, - }); - instantiationService.stub(IProductService, {}); - instantiationService.stub(ITelemetryService, NullTelemetryService); - const uriIdentityService = instantiationService.stub(IUriIdentityService, disposables.add(instantiationService.createInstance(UriIdentityService))); - const userDataProfilesService = instantiationService.stub(IUserDataProfilesService, disposables.add(instantiationService.createInstance(UserDataProfilesService))); - const extensionsProfileScannerService = instantiationService.stub(IExtensionsProfileScannerService, disposables.add(instantiationService.createInstance(ExtensionsProfileScannerService))); - const extensionsScannerService = instantiationService.stub(IExtensionsScannerService, disposables.add(instantiationService.createInstance(ExtensionsScannerService))); - super( - { - name: extension.name, - publisher: extension.publisher, - version: extension.version, - engines: { vscode: '*' }, - }, - extension, - { profileLocation: userDataProfilesService.defaultProfile.extensionsResource, productVersion: { version: '' } }, - extensionDownloader, - new TestExtensionsScanner(), - uriIdentityService, - userDataProfilesService, - extensionsScannerService, - extensionsProfileScannerService, - logService, - NullTelemetryService - ); - } - - protected override async doRun(token: CancellationToken): Promise { - const result = await this.install(token); - return result[0]; - } - - protected override async extractExtension(): Promise { - this.installed = true; - return new class extends mock() { }; - } - - protected override async validateManifest(): Promise { } -} - -suite('InstallGalleryExtensionTask Tests', () => { - - const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - - test('if verification is enabled by default, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, true); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification is enabled in stable, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true, quality: 'stable' }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, true); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification is disabled by setting set to false, the task skips verification', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: false, verificationResult: 'error' }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification is disabled because the module is not loaded, the task skips verification', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: false }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification fails to execute, the task completes', async () => { - const errorCode = 'ENOENT'; - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, errorCode); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification fails', async () => { - const errorCode = 'IntegrityCheckFailed'; - - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: errorCode }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, errorCode); - assert.strictEqual(testObject.installed, true); - }); - - test('if verification succeeds, the task completes', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, true); - assert.strictEqual(testObject.installed, true); - }); - - test('task completes for unsigned extension', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: true }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - test('task completes for an unsigned extension even when signature verification throws error', async () => { - const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader({ isSignatureVerificationEnabled: true, verificationResult: 'error' }), disposables.add(new DisposableStore())); - - await testObject.run(); - - assert.strictEqual(testObject.verificationStatus, false); - assert.strictEqual(testObject.installed, true); - }); - - function anExtensionsDownloader(options: { isSignatureVerificationEnabled: boolean; verificationResult: boolean | string; quality?: string }): ExtensionsDownloader { - const logService = new NullLogService(); - const fileService = disposables.add(new FileService(logService)); - const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider)); - - const instantiationService = disposables.add(new TestInstantiationService()); - instantiationService.stub(IProductService, { quality: options.quality ?? 'insiders' }); - instantiationService.stub(IFileService, fileService); - instantiationService.stub(ILogService, logService); - instantiationService.stub(INativeEnvironmentService, { extensionsDownloadLocation: joinPath(ROOT, 'CachedExtensionVSIXs') }); - instantiationService.stub(IExtensionGalleryService, { - async download(extension, location, operation) { - await fileService.writeFile(location, VSBuffer.fromString('extension vsix')); - }, - async downloadSignatureArchive(extension, location) { - await fileService.writeFile(location, VSBuffer.fromString('extension signature')); - }, - }); - instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); - instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult)); - return disposables.add(instantiationService.createInstance(ExtensionsDownloader)); - } - - function aGalleryExtension(name: string, properties: Partial = {}, galleryExtensionProperties: any = {}, assets: Partial = {}): IGalleryExtension { - const targetPlatform = getTargetPlatform(platform, arch); - const galleryExtension = Object.create({ name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [targetPlatform], properties: {}, assets: {}, ...properties }); - galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], targetPlatform, ...galleryExtensionProperties }; - galleryExtension.assets = { ...galleryExtension.assets, ...assets }; - galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() }; - return galleryExtension; - } -}); diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index a81954da0f7..05ff156de89 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -88,6 +88,11 @@ export function isRecursiveWatchRequest(request: IWatchRequest): request is IRec export type IUniversalWatchRequest = IRecursiveWatchRequest | INonRecursiveWatchRequest; +export interface IWatcherErrorEvent { + readonly error: string; + readonly request?: IUniversalWatchRequest; +} + export interface IWatcher { /** @@ -106,7 +111,7 @@ export interface IWatcher { * that is unrecoverable. Listeners should restart the * watcher if possible. */ - readonly onDidError: Event; + readonly onDidError: Event; /** * Configures the watcher to watch according to the @@ -176,22 +181,24 @@ export interface IUniversalWatcher extends IWatcher { export abstract class AbstractWatcherClient extends Disposable { - private static readonly MAX_RESTARTS = 5; + 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 watcher: IWatcher | undefined; private readonly watcherDisposables = this._register(new MutableDisposable()); private requests: IWatchRequest[] | undefined = undefined; - private restartCounter = 0; + private restartsPerRequestError = new Map(); + private restartsPerUnknownError = 0; constructor( private readonly onFileChanges: (changes: IFileChange[]) => void, private readonly onLogMessage: (msg: ILogMessage) => void, private verboseLogging: boolean, private options: { - type: string; - restartOnError: boolean; + readonly type: string; + readonly restartOnError: boolean; } ) { super(); @@ -212,18 +219,36 @@ export abstract class AbstractWatcherClient extends Disposable { // Wire in event handlers disposables.add(this.watcher.onDidChangeFile(changes => this.onFileChanges(changes))); disposables.add(this.watcher.onDidLogMessage(msg => this.onLogMessage(msg))); - disposables.add(this.watcher.onDidError(error => this.onError(error))); + disposables.add(this.watcher.onDidError(e => this.onError(e.error, e.request))); } - protected onError(error: string): void { + protected onError(error: string, failedRequest?: IUniversalWatchRequest): void { // Restart on error (up to N times, if enabled) - if (this.options.restartOnError) { - if (this.restartCounter < AbstractWatcherClient.MAX_RESTARTS && this.requests) { - this.error(`restarting watcher after error: ${error}`); - this.restart(this.requests); - } else { - this.error(`gave up attempting to restart watcher after error: ${error}`); + 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}`); + } } } @@ -234,8 +259,6 @@ export abstract class AbstractWatcherClient extends Disposable { } private restart(requests: IUniversalWatchRequest[]): void { - this.restartCounter++; - this.init(); this.watch(requests); } diff --git a/src/vs/platform/files/node/watcher/baseWatcher.ts b/src/vs/platform/files/node/watcher/baseWatcher.ts index f80ac506d98..3576f20d4f7 100644 --- a/src/vs/platform/files/node/watcher/baseWatcher.ts +++ b/src/vs/platform/files/node/watcher/baseWatcher.ts @@ -5,7 +5,7 @@ import { watchFile, unwatchFile, Stats } from 'fs'; import { Disposable, DisposableMap, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { ILogMessage, IRecursiveWatcherWithSubscribe, IUniversalWatchRequest, IWatchRequestWithCorrelation, IWatcher, isWatchRequestWithCorrelation, requestFilterToString } from 'vs/platform/files/common/watcher'; +import { ILogMessage, IRecursiveWatcherWithSubscribe, IUniversalWatchRequest, IWatchRequestWithCorrelation, IWatcher, IWatcherErrorEvent, isWatchRequestWithCorrelation, requestFilterToString } from 'vs/platform/files/common/watcher'; import { Emitter, Event } from 'vs/base/common/event'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; @@ -254,7 +254,7 @@ export abstract class BaseWatcher extends Disposable implements IWatcher { protected abstract trace(message: string): void; protected abstract warn(message: string): void; - abstract onDidError: Event; + abstract onDidError: Event; protected verboseLogging = false; diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 1e37e8d4622..afabd7aed12 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -21,7 +21,7 @@ import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { FileChangeType, IFileChange } from 'vs/platform/files/common/files'; -import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered } from 'vs/platform/files/common/watcher'; +import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from 'vs/platform/files/common/watcher'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export class ParcelWatcherInstance extends Disposable { @@ -147,7 +147,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events'; - private readonly _onDidError = this._register(new Emitter()); + private readonly _onDidError = this._register(new Emitter()); readonly onDidError = this._onDidError.event; readonly watchers = new Set(); @@ -359,7 +359,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // 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, watcher); + this.onUnexpectedError(error, request); } // Handle & emit events @@ -372,7 +372,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS instance.complete(parcelWatcher); }).catch(error => { - this.onUnexpectedError(error, watcher); + this.onUnexpectedError(error, request); instance.complete(undefined); @@ -607,7 +607,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS return false; } - private onUnexpectedError(error: unknown, watcher?: ParcelWatcherInstance): void { + private onUnexpectedError(error: unknown, request?: IRecursiveWatchRequest): void { const msg = toErrorMessage(error); // Specially handle ENOSPC errors that can happen when @@ -617,7 +617,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // See https://github.com/microsoft/vscode/issues/7950 if (msg.indexOf('No space left on device') !== -1) { if (!this.enospcErrorLogged) { - this.error('Inotify limit reached (ENOSPC)', watcher); + this.error('Inotify limit reached (ENOSPC)', request); this.enospcErrorLogged = true; } @@ -627,9 +627,9 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // restart the watcher as a result to get into healthy // state again if possible and if not attempted too much else { - this.error(`Unexpected error: ${msg} (EUNKNOWN)`, watcher); + this.error(`Unexpected error: ${msg} (EUNKNOWN)`, request); - this._onDidError.fire(msg); + this._onDidError.fire({ request, error: msg }); } } @@ -681,7 +681,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS try { await watcher.stop(joinRestart); } catch (error) { - this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher); + this.error(`Unexpected error stopping watcher: ${toErrorMessage(error)}`, watcher.request); } } @@ -820,20 +820,20 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS protected trace(message: string, watcher?: ParcelWatcherInstance): void { if (this.verboseLogging) { - this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher) }); + this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message, watcher?.request) }); } } protected warn(message: string, watcher?: ParcelWatcherInstance) { - this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) }); + this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher?.request) }); } - private error(message: string, watcher?: ParcelWatcherInstance) { - this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, watcher) }); + private error(message: string, request?: IRecursiveWatchRequest) { + this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, request) }); } - private toMessage(message: string, watcher?: ParcelWatcherInstance): string { - return watcher ? `[File Watcher (parcel)] ${message} (path: ${watcher.request.path})` : `[File Watcher (parcel)] ${message}`; + private toMessage(message: string, request?: IRecursiveWatchRequest): string { + return request ? `[File Watcher (parcel)] ${message} (path: ${request.path})` : `[File Watcher (parcel)] ${message}`; } protected get recursiveWatcher() { return this; } diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index 2e585612589..836b67ed9d2 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -701,7 +701,7 @@ import { TestParcelWatcher } from 'vs/platform/files/test/node/parcelWatcher.int recursiveWatcher.onDidError(e => { if (loggingEnabled) { - console.log(`[recursive watcher test error] ${e}`); + console.log(`[recursive watcher test error] ${e.error}`); } }); diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 2f051861dd8..db85e28f341 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -87,7 +87,7 @@ export class TestParcelWatcher extends ParcelWatcher { watcher.onDidError(e => { if (loggingEnabled) { - console.log(`[recursive watcher test error] ${e}`); + console.log(`[recursive watcher test error] ${e.error}`); } }); @@ -743,7 +743,7 @@ export class TestParcelWatcher extends ParcelWatcher { await testCorrelatedWatchFolderDoesNotExist(false); }); - test('correlated watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => { + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, does not exist in beginning, reusing watcher)', async () => { await testCorrelatedWatchFolderDoesNotExist(true); }); @@ -798,7 +798,7 @@ export class TestParcelWatcher extends ParcelWatcher { await testCorrelatedWatchFolderExists(false); }); - test('correlated watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => { + (!isMacintosh /* Linux/Windows: times out for some reason */ ? test.skip : test)('correlated watch requests support suspend/resume (folder, exist in beginning, reusing watcher)', async () => { await testCorrelatedWatchFolderExists(true); }); diff --git a/src/vs/platform/hover/test/browser/nullHoverService.ts b/src/vs/platform/hover/test/browser/nullHoverService.ts index 1040ba4d772..6b7b728325b 100644 --- a/src/vs/platform/hover/test/browser/nullHoverService.ts +++ b/src/vs/platform/hover/test/browser/nullHoverService.ts @@ -12,4 +12,5 @@ export const NullHoverService: IHoverService = { showHover: () => undefined, setupUpdatableHover: () => Disposable.None as any, showAndFocusLastHover: () => undefined, + triggerUpdatableHover: () => undefined }; diff --git a/src/vs/platform/instantiation/common/instantiation.ts b/src/vs/platform/instantiation/common/instantiation.ts index 86766a4a6c6..c1ce6565271 100644 --- a/src/vs/platform/instantiation/common/instantiation.ts +++ b/src/vs/platform/instantiation/common/instantiation.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DisposableStore } from 'vs/base/common/lifecycle'; import * as descriptors from './descriptors'; import { ServiceCollection } from './serviceCollection'; @@ -61,8 +62,11 @@ export interface IInstantiationService { /** * Creates a child of this service which inherits all current services * and adds/overwrites the given services. + * + * NOTE that the returned child is `disposable` and should be disposed when not used + * anymore. This will also dispose all the services that this service has created. */ - createChild(services: ServiceCollection): IInstantiationService; + createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService; /** * Disposes this instantiation service. diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index fd95305865a..4b313ed32eb 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -6,7 +6,7 @@ import { GlobalIdleValue } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { illegalState } from 'vs/base/common/errors'; -import { dispose, IDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, isDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { SyncDescriptor, SyncDescriptor0 } from 'vs/platform/instantiation/common/descriptors'; import { Graph } from 'vs/platform/instantiation/common/graph'; import { GetLeadingNonServiceArgs, IInstantiationService, ServiceIdentifier, ServicesAccessor, _util } from 'vs/platform/instantiation/common/instantiation'; @@ -70,16 +70,19 @@ export class InstantiationService implements IInstantiationService { } } - createChild(services: ServiceCollection): IInstantiationService { + createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService { this._throwIfDisposed(); + const that = this; const result = new class extends InstantiationService { override dispose(): void { - this._children.delete(result); + that._children.delete(result); super.dispose(); } }(services, this._strict, this, this._enableTracing); this._children.add(result); + + store?.add(result); return result; } diff --git a/src/vs/platform/keybinding/common/keybindingsRegistry.ts b/src/vs/platform/keybinding/common/keybindingsRegistry.ts index 4ab820a3732..6c88451dff6 100644 --- a/src/vs/platform/keybinding/common/keybindingsRegistry.ts +++ b/src/vs/platform/keybinding/common/keybindingsRegistry.ts @@ -43,6 +43,9 @@ export interface IKeybindingRule extends IKeybindings { id: string; weight: number; args?: any; + /** + * Keybinding is disabled if expression returns false. + */ when?: ContextKeyExpression | null | undefined; } diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 94ae20b32f8..85e831fa04a 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -77,6 +77,7 @@ export interface ICommonNativeHostService { openWindow(options?: IOpenEmptyWindowOptions): Promise; openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; + isFullScreen(options?: INativeHostOptions): Promise; toggleFullScreen(options?: INativeHostOptions): Promise; handleTitleDoubleClick(options?: INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 64737b07d00..f7235bd6834 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -212,6 +212,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain }, options); } + async isFullScreen(windowId: number | undefined, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + return window?.isFullScreen ?? false; + } + async toggleFullScreen(windowId: number | undefined, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); window?.toggleFullScreen(); diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts new file mode 100644 index 00000000000..096993beb80 --- /dev/null +++ b/src/vs/platform/observable/common/platformObservableUtils.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { autorunOpts, IObservable, IReader, observableFromEvent } from 'vs/base/common/observable'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyValue, RawContextKey, IContextKeyService } 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( + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key) ?? defaultValue + ); +} + +/** Update the configuration key with a value derived from observables. */ +export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { + const boundKey = key.bindTo(service); + return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { + boundKey.set(computeValue(reader)); + }); +} + diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 467e11cc56a..eb383dadcb5 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -58,7 +58,7 @@ else { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.87.0-dev', + version: '1.90.0-dev', nameShort: 'Code - OSS Dev', nameLong: 'Code - OSS Dev', applicationName: 'code-oss', diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 86160c9e26a..0613c557abc 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -326,6 +326,14 @@ export abstract class PickerQuickAccessProvider { + if (runOptions?.handleAccept) { + if (!event.inBackground) { + picker.hide(); // hide picker unless we accept in background + } + runOptions.handleAccept?.(picker.activeItems[0]); + return; + } + const [item] = picker.selectedItems; if (typeof item?.accept === 'function') { if (!event.inBackground) { diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index 88169c32ed5..7cccc352393 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -8,7 +8,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { DefaultQuickAccessFilterValue, Extensions, IQuickAccessController, IQuickAccessOptions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuickAccessProviderRunOptions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; +import { DefaultQuickAccessFilterValue, Extensions, IQuickAccessController, IQuickAccessOptions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; import { IQuickInputService, IQuickPick, IQuickPickItem, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -45,7 +45,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon private doShowOrPick(value: string, pick: boolean, options?: IQuickAccessOptions): Promise | void { // Find provider for the value to show - const [provider, descriptor] = this.getOrInstantiateProvider(value); + const [provider, descriptor] = this.getOrInstantiateProvider(value, options?.enabledProviderPrefixes); // Return early if quick access is already showing on that same prefix const visibleQuickAccess = this.visibleQuickAccess; @@ -102,7 +102,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon const picker = disposables.add(this.quickInputService.createQuickPick()); picker.value = value; this.adjustValueSelection(picker, descriptor, options); - picker.placeholder = descriptor?.placeholder; + picker.placeholder = options?.placeholder ?? descriptor?.placeholder; picker.quickNavigate = options?.quickNavigateConfiguration; picker.hideInput = !!picker.quickNavigate && !visibleQuickAccess; // only hide input if there was no picker opened already if (typeof options?.itemActivation === 'number' || options?.quickNavigateConfiguration) { @@ -123,7 +123,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon } // Register listeners - disposables.add(this.registerPickerListeners(picker, provider, descriptor, value, options?.providerOptions)); + disposables.add(this.registerPickerListeners(picker, provider, descriptor, value, options)); // Ask provider to fill the picker as needed if we have one // and pass over a cancellation token that will indicate when @@ -184,7 +184,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon provider: IQuickAccessProvider | undefined, descriptor: IQuickAccessProviderDescriptor | undefined, value: string, - providerOptions?: IQuickAccessProviderRunOptions + options?: IQuickAccessOptions ): IDisposable { const disposables = new DisposableStore(); @@ -199,13 +199,14 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon // Whenever the value changes, check if the provider has // changed and if so - re-create the picker from the beginning disposables.add(picker.onDidChangeValue(value => { - const [providerForValue] = this.getOrInstantiateProvider(value); + const [providerForValue] = this.getOrInstantiateProvider(value, options?.enabledProviderPrefixes); if (providerForValue !== provider) { this.show(value, { + enabledProviderPrefixes: options?.enabledProviderPrefixes, // do not rewrite value from user typing! preserveValue: true, // persist the value of the providerOptions from the original showing - providerOptions + providerOptions: options?.providerOptions }); } else { visibleQuickAccess.value = value; // remember the value in our visible one @@ -222,9 +223,9 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon return disposables; } - private getOrInstantiateProvider(value: string): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] { + private getOrInstantiateProvider(value: string, enabledProviderPrefixes?: string[]): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] { const providerDescriptor = this.registry.getQuickAccessProvider(value); - if (!providerDescriptor) { + if (!providerDescriptor || enabledProviderPrefixes && !enabledProviderPrefixes?.includes(providerDescriptor.prefix)) { return [undefined, undefined]; } diff --git a/src/vs/platform/quickinput/browser/quickInputTree.ts b/src/vs/platform/quickinput/browser/quickInputTree.ts index 80e68364349..3a492f78a1e 100644 --- a/src/vs/platform/quickinput/browser/quickInputTree.ts +++ b/src/vs/platform/quickinput/browser/quickInputTree.ts @@ -38,6 +38,7 @@ 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 { QuickPickFocus } from '../common/quickInput'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const $ = dom.$; @@ -706,7 +707,8 @@ export class QuickInputTree extends Disposable { private hoverDelegate: IHoverDelegate, private linkOpenerDelegate: (content: string) => void, id: string, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(); this._container = dom.append(this.parent, $('.quick-input-list')); @@ -1114,6 +1116,20 @@ export class QuickInputTree extends Disposable { } this._tree.setChildren(null, elements); this._onChangedVisibleCount.fire(visibleCount); + + // Accessibility hack, unfortunately on next tick + // https://github.com/microsoft/vscode/issues/211976 + if (this.accessibilityService.isScreenReaderOptimized()) { + setTimeout(() => { + const focusedElement = this._tree.getHTMLElement().querySelector(`.monaco-list-row.focused`); + const parent = focusedElement?.parentNode; + if (focusedElement && parent) { + const nextSibling = focusedElement.nextSibling; + parent.removeChild(focusedElement); + parent.insertBefore(focusedElement, nextSibling); + } + }, 0); + } } getElementsCount(): number { diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index c160bb1fb93..4fd3faf30e0 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -15,6 +15,13 @@ import { Registry } from 'vs/platform/registry/common/platform'; */ export interface IQuickAccessProviderRunOptions { readonly from?: string; + readonly placeholder?: string; + /** + * A handler to invoke when an item is accepted for + * this particular showing of the quick access. + * @param item The item that was accepted. + */ + readonly handleAccept?: (item: IQuickPickItem) => void; } /** @@ -22,6 +29,8 @@ 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. @@ -53,6 +62,17 @@ export interface IQuickAccessOptions { * quick access. */ readonly providerOptions?: IQuickAccessProviderRunOptions; + + /** + * An array of provider prefixes to enable for this + * particular showing of the quick access. + */ + readonly enabledProviderPrefixes?: string[]; + + /** + * A placeholder to use for this particular showing of the quick access. + */ + readonly placeholder?: string; } export interface IQuickAccessController { diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index 216aa3a7f54..8e8fc694be0 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -32,6 +32,8 @@ import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyServ import { NoMatchingKb } from 'vs/platform/keybinding/common/keybindingResolver'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { TestAccessibilityService } from 'vs/platform/accessibility/test/common/testAccessibilityService'; // Sets up an `onShow` listener to allow us to wait until the quick pick is shown (useful when triggering an `accept()` right after launching a quick pick) // kick this off before you launch the picker and then await the promise returned after you launch the picker. @@ -62,6 +64,7 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 // Stub the services the quick input controller needs to function instantiationService.stub(IThemeService, new TestThemeService()); instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IAccessibilityService, new TestAccessibilityService()); instantiationService.stub(IListService, store.add(new ListService())); instantiationService.stub(ILayoutService, { activeContainer: fixture, onDidLayoutContainer: Event.None } as any); instantiationService.stub(IContextViewService, store.add(instantiationService.createInstance(ContextViewService))); diff --git a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts index 30dc15f3db5..d0d433a33e6 100644 --- a/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts +++ b/src/vs/platform/remoteTunnel/node/remoteTunnelService.ts @@ -306,7 +306,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ a = a.replaceAll(token, '*'.repeat(4)); onOutput(a, isErr); }; - const loginProcess = this.runCodeTunnelCommand('login', ['user', 'login', '--provider', session.providerId, '--access-token', token, '--log', LogLevelToString(this._logger.getLevel())], onLoginOutput); + const loginProcess = this.runCodeTunnelCommand('login', ['user', 'login', '--provider', session.providerId, '--log', LogLevelToString(this._logger.getLevel())], onLoginOutput, { VSCODE_CLI_ACCESS_TOKEN: token }); this._tunnelProcess = loginProcess; try { await loginProcess; @@ -408,7 +408,7 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ }); } - private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = this.defaultOnOutput): CancelablePromise { + private runCodeTunnelCommand(logLabel: string, commandArgs: string[], onOutput: (message: string, isError: boolean) => void = this.defaultOnOutput, env?: Record): CancelablePromise { return createCancelablePromise(token => { return new Promise((resolve, reject) => { if (token.isCancellationRequested) { @@ -426,12 +426,12 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ if (!this.environmentService.isBuilt) { onOutput('Building tunnel CLI from sources and run\n', false); onOutput(`${logLabel} Spawning: cargo run -- tunnel ${commandArgs.join(' ')}\n`, false); - tunnelProcess = spawn('cargo', ['run', '--', 'tunnel', ...commandArgs], { cwd: join(this.environmentService.appRoot, 'cli'), stdio }); + tunnelProcess = spawn('cargo', ['run', '--', 'tunnel', ...commandArgs], { cwd: join(this.environmentService.appRoot, 'cli'), stdio, env: { ...process.env, RUST_BACKTRACE: '1', ...env } }); } else { onOutput('Running tunnel CLI\n', false); const tunnelCommand = this.getTunnelCommandLocation(); onOutput(`${logLabel} Spawning: ${tunnelCommand} tunnel ${commandArgs.join(' ')}\n`, false); - tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio }); + tunnelProcess = spawn(tunnelCommand, ['tunnel', ...commandArgs], { cwd: homedir(), stdio, env: { ...process.env, ...env } }); } tunnelProcess.stdout!.pipe(new StreamSplitter('\n')).on('data', data => { diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 087f5858b48..5cb80e342cc 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -19,6 +19,7 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil import { parseSharedProcessDebugPort } from 'vs/platform/environment/node/environmentService'; import { assertIsDefined } from 'vs/base/common/types'; import { SharedProcessChannelConnection, SharedProcessRawConnection, SharedProcessLifecycle } from 'vs/platform/sharedProcess/common/sharedProcess'; +import { Emitter } from 'vs/base/common/event'; export class SharedProcess extends Disposable { @@ -27,9 +28,13 @@ export class SharedProcess extends Disposable { private utilityProcess: UtilityProcess | undefined = undefined; private utilityProcessLogListener: IDisposable | undefined = undefined; + private readonly _onDidCrash = this._register(new Emitter()); + readonly onDidCrash = this._onDidCrash.event; + constructor( private readonly machineId: string, private readonly sqmId: string, + private readonly devDeviceId: string, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @@ -168,12 +173,15 @@ export class SharedProcess extends Disposable { payload: this.createSharedProcessConfiguration(), execArgv }); + + this._register(this.utilityProcess.onCrash(() => this._onDidCrash.fire())); } private createSharedProcessConfiguration(): ISharedProcessConfiguration { return { machineId: this.machineId, sqmId: this.sqmId, + devDeviceId: this.devDeviceId, codeCachePath: this.environmentMainService.codeCachePath, profiles: { home: this.userDataProfilesService.profilesHome, diff --git a/src/vs/platform/sharedProcess/node/sharedProcess.ts b/src/vs/platform/sharedProcess/node/sharedProcess.ts index f93082d7a2d..0c0e49c3a0b 100644 --- a/src/vs/platform/sharedProcess/node/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/node/sharedProcess.ts @@ -15,6 +15,8 @@ export interface ISharedProcessConfiguration { readonly sqmId: string; + readonly devDeviceId: string; + readonly codeCachePath: string | undefined; readonly args: NativeParsedArgs; diff --git a/src/vs/platform/telemetry/common/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts index 9ae3427a07b..b649cb80775 100644 --- a/src/vs/platform/telemetry/common/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -24,6 +24,7 @@ export function resolveCommonProperties( version: string | undefined, machineId: string | undefined, sqmId: string | undefined, + devDeviceId: string | undefined, isInternalTelemetry: boolean, product?: string ): ICommonProperties { @@ -33,6 +34,8 @@ export function resolveCommonProperties( result['common.machineId'] = machineId; // __GDPR__COMMON__ "common.sqmId" : { "endPoint": "SqmMachineId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" } result['common.sqmId'] = sqmId; + // __GDPR__COMMON__ "common.devDeviceId" : { "endPoint": "SqmMachineId", "classification": "EndUserPseudonymizedInformation", "purpose": "BusinessInsight" } + result['common.devDeviceId'] = devDeviceId; // __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['sessionID'] = generateUuid() + Date.now(); // __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 73db745352a..d6b4179b71f 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -23,6 +23,7 @@ export interface ITelemetryService { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; + readonly devDeviceId: string; readonly firstSessionDate: string; readonly msftInternal?: boolean; @@ -73,6 +74,7 @@ export const firstSessionDateStorageKey = 'telemetry.firstSessionDate'; export const lastSessionDateStorageKey = 'telemetry.lastSessionDate'; export const machineIdKey = 'telemetry.machineId'; export const sqmIdKey = 'telemetry.sqmId'; +export const devDeviceIdKey = 'telemetry.devDeviceId'; // Configuration Keys export const TELEMETRY_SECTION_ID = 'telemetry'; diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index a9a9cc48109..245bb41b5d8 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -34,6 +34,7 @@ export class TelemetryService implements ITelemetryService { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; + readonly devDeviceId: string; readonly firstSessionDate: string; readonly msftInternal: boolean | undefined; @@ -58,6 +59,7 @@ export class TelemetryService implements ITelemetryService { this.sessionId = this._commonProperties['sessionID'] as string; this.machineId = this._commonProperties['common.machineId'] as string; this.sqmId = this._commonProperties['common.sqmId'] as string; + this.devDeviceId = this._commonProperties['common.devDeviceId'] as string; this.firstSessionDate = this._commonProperties['common.firstSessionDate'] as string; this.msftInternal = this._commonProperties['common.msftInternal'] as boolean | undefined; diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index c26c33886cc..ec72e1a0829 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -30,6 +30,7 @@ export class NullTelemetryServiceShape implements ITelemetryService { readonly sessionId = 'someValue.sessionId'; readonly machineId = 'someValue.machineId'; readonly sqmId = 'someValue.sqmId'; + readonly devDeviceId = 'someValue.devDeviceId'; readonly firstSessionDate = 'someValue.firstSessionDate'; readonly sendErrorTelemetry = false; publicLog() { } diff --git a/src/vs/platform/telemetry/electron-main/telemetryUtils.ts b/src/vs/platform/telemetry/electron-main/telemetryUtils.ts index 6dc9a9fa9d6..95e993f8462 100644 --- a/src/vs/platform/telemetry/electron-main/telemetryUtils.ts +++ b/src/vs/platform/telemetry/electron-main/telemetryUtils.ts @@ -5,8 +5,8 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IStateService } from 'vs/platform/state/node/state'; -import { machineIdKey, sqmIdKey } from 'vs/platform/telemetry/common/telemetry'; -import { resolveMachineId as resolveNodeMachineId, resolveSqmId as resolveNodeSqmId } from 'vs/platform/telemetry/node/telemetryUtils'; +import { machineIdKey, sqmIdKey, devDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; +import { resolveMachineId as resolveNodeMachineId, resolveSqmId as resolveNodeSqmId, resolvedevDeviceId as resolveNodedevDeviceId } from 'vs/platform/telemetry/node/telemetryUtils'; export async function resolveMachineId(stateService: IStateService, logService: ILogService): Promise { // Call the node layers implementation to avoid code duplication @@ -20,3 +20,9 @@ export async function resolveSqmId(stateService: IStateService, logService: ILog stateService.setItem(sqmIdKey, sqmId); return sqmId; } + +export async function resolvedevDeviceId(stateService: IStateService, logService: ILogService): Promise { + const devDeviceId = await resolveNodedevDeviceId(stateService, logService); + stateService.setItem(devDeviceIdKey, devDeviceId); + return devDeviceId; +} diff --git a/src/vs/platform/telemetry/node/telemetryUtils.ts b/src/vs/platform/telemetry/node/telemetryUtils.ts index cb5a03fd687..4f8fa8c8a5b 100644 --- a/src/vs/platform/telemetry/node/telemetryUtils.ts +++ b/src/vs/platform/telemetry/node/telemetryUtils.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { isMacintosh } from 'vs/base/common/platform'; -import { getMachineId, getSqmMachineId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { ILogService } from 'vs/platform/log/common/log'; import { IStateReadService } from 'vs/platform/state/node/state'; -import { machineIdKey, sqmIdKey } from 'vs/platform/telemetry/common/telemetry'; +import { machineIdKey, sqmIdKey, devDeviceIdKey } from 'vs/platform/telemetry/common/telemetry'; export async function resolveMachineId(stateService: IStateReadService, logService: ILogService): Promise { @@ -29,3 +29,12 @@ export async function resolveSqmId(stateService: IStateReadService, logService: return sqmId; } + +export async function resolvedevDeviceId(stateService: IStateReadService, logService: ILogService): Promise { + let devDeviceId = stateService.getItem(devDeviceIdKey); + if (typeof devDeviceId !== 'string') { + devDeviceId = await getdevDeviceId(logService.error.bind(logService)); + } + + return devDeviceId; +} diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 79be9013c81..a799870b548 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -102,7 +102,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess private _processStartupComplete: Promise | undefined; private _windowsShellHelper: WindowsShellHelper | undefined; private _childProcessMonitor: ChildProcessMonitor | undefined; - private _titleInterval: NodeJS.Timer | null = null; + private _titleInterval: NodeJS.Timeout | null = null; private _writeQueue: IWriteObject[] = []; private _writeTimeout: NodeJS.Timeout | undefined; private _delayedResizer: DelayedResizer | undefined; diff --git a/src/vs/platform/theme/browser/defaultStyles.ts b/src/vs/platform/theme/browser/defaultStyles.ts index 7549840266a..871178faf38 100644 --- a/src/vs/platform/theme/browser/defaultStyles.ts +++ b/src/vs/platform/theme/browser/defaultStyles.ts @@ -21,8 +21,8 @@ export type IStyleOverride = { [P in keyof T]?: ColorIdentifier | undefined; }; -function overrideStyles(override: IStyleOverride, styles: T): any { - const result = { ...styles } as { [P in keyof T]: string | undefined }; +function overrideStyles(override: IStyleOverride, styles: T): any { + const result: { [P in keyof T]: string | undefined } = { ...styles }; for (const key in override) { const val = override[key]; result[key] = val !== undefined ? asCssVariable(val) : undefined; diff --git a/src/vs/platform/userDataProfile/common/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfile.ts index 769f5fd2536..b65c078f83a 100644 --- a/src/vs/platform/userDataProfile/common/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfile.ts @@ -35,6 +35,7 @@ export const enum ProfileResourceType { * Flags to indicate whether to use the default profile or not. */ export type UseDefaultProfileFlags = { [key in ProfileResourceType]?: boolean }; +export type ProfileResourceTypeFlags = UseDefaultProfileFlags; export interface IUserDataProfile { readonly id: string; diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index be0c0e0bbb4..53561e249db 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; -import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension, ExtensionManagementError, ExtensionManagementErrorCode, IGalleryExtension, DISABLED_EXTENSIONS_STORAGE_PATH, EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, ExtensionInstallSource } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { ExtensionType, IExtensionIdentifier, isApplicationScopedExtension } from 'vs/platform/extensions/common/extensions'; @@ -483,10 +483,11 @@ export class LocalExtensionsProvider { isMachineScoped: false /* set isMachineScoped value to prevent install and sync dialog in web */, donotIncludePackAndDependencies: true, installGivenVersion: e.pinned && !!e.version, + pinned: e.pinned, installPreReleaseVersion: e.preRelease, profileLocation: profile.extensionsResource, isApplicationScoped: e.isApplicationScoped, - context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SYNC_CONTEXT]: true } + context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.SETTINGS_SYNC } } }); syncExtensionsToInstall.set(extension.identifier.id.toLowerCase(), e); @@ -569,7 +570,7 @@ export class LocalExtensionsProvider { return this.userDataProfileStorageService.withProfileScopedStorageService(profile, async storageService => { const disposables = new DisposableStore(); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService])); + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService]))); const extensionEnablementService = disposables.add(instantiationService.createInstance(GlobalExtensionEnablementService)); const extensionStorageService = disposables.add(instantiationService.createInstance(ExtensionStorageService)); try { diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index eba3ad14980..856003580d0 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { equals } from 'vs/base/common/arrays'; -import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; @@ -98,6 +98,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this._status = userDataSyncStoreManagementService.userDataSyncStore ? SyncStatus.Idle : SyncStatus.Uninitialized; this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.APPLICATION, undefined); this._register(toDisposable(() => this.clearActiveProfileSynchronizers())); + + this._register(new RunOnceScheduler(() => this.cleanUpStaleStorageData(), 5 * 1000 /* after 5s */)).schedule(); } async createSyncTask(manifest: IUserDataManifest | null, disableCache?: boolean): Promise { @@ -245,6 +247,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (this.userDataProfilesService.profiles.some(p => p.id === profileSynchronizerItem[0].profile.id)) { continue; } + await profileSynchronizerItem[0].resetLocal(); profileSynchronizerItem[1].dispose(); this.activeProfileSynchronizers.delete(key); } @@ -397,6 +400,46 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.logService.info('Did reset the local sync state.'); } + private async cleanUpStaleStorageData(): Promise { + const allKeys = this.storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE); + const lastSyncProfileKeys: [string, string][] = []; + for (const key of allKeys) { + if (!key.endsWith('.lastSyncUserData')) { + continue; + } + const segments = key.split('.'); + if (segments.length === 3) { + lastSyncProfileKeys.push([key, segments[0]]); + } + } + if (!lastSyncProfileKeys.length) { + return; + } + + const disposables = new DisposableStore(); + + try { + let defaultProfileSynchronizer = this.activeProfileSynchronizers.get(this.userDataProfilesService.defaultProfile.id)?.[0]; + if (!defaultProfileSynchronizer) { + defaultProfileSynchronizer = disposables.add(this.instantiationService.createInstance(ProfileSynchronizer, this.userDataProfilesService.defaultProfile, undefined)); + } + const userDataProfileManifestSynchronizer = defaultProfileSynchronizer.enabled.find(s => s.resource === SyncResource.Profiles) as UserDataProfilesManifestSynchroniser; + if (!userDataProfileManifestSynchronizer) { + return; + } + const lastSyncedProfiles = await userDataProfileManifestSynchronizer.getLastSyncedProfiles(); + const lastSyncedCollections = lastSyncedProfiles?.map(p => p.collection) ?? []; + for (const [key, collection] of lastSyncProfileKeys) { + if (!lastSyncedCollections.includes(collection)) { + this.logService.info(`Removing last sync state for stale profile: ${collection}`); + this.storageService.remove(key, StorageScope.APPLICATION); + } + } + } finally { + disposables.dispose(); + } + } + async cleanUpRemoteData(): Promise { const remoteProfiles = await this.userDataSyncResourceProviderService.getRemoteSyncedProfiles(); const remoteProfileCollections = remoteProfiles.map(profile => profile.collection); diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 8adf80ad9c7..ae20d9d7620 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -348,6 +348,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native machineId: string; sqmId: string; + devDeviceId: string; execPath: string; backupPath?: string; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 2c4a799c893..a4cb0ca698b 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -211,6 +211,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic constructor( private readonly machineId: string, private readonly sqmId: string, + private readonly devDeviceId: string, private readonly initialUserEnv: IProcessEnvironment, @ILogService private readonly logService: ILogService, @ILoggerMainService private readonly loggerService: ILoggerMainService, @@ -1409,6 +1410,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic machineId: this.machineId, sqmId: this.sqmId, + devDeviceId: this.devDeviceId, windowId: -1, // Will be filled in by the window once loaded later diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 84664bbb39a..9eace08d06a 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -67,7 +67,7 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { private readonly _serverRootPath: string; - private shutdownTimer: NodeJS.Timer | undefined; + private shutdownTimer: NodeJS.Timeout | undefined; constructor( private readonly _socketServer: SocketServer, diff --git a/src/vs/server/node/remoteExtensionsScanner.ts b/src/vs/server/node/remoteExtensionsScanner.ts index 95e9c62afc8..54418aebfc2 100644 --- a/src/vs/server/node/remoteExtensionsScanner.ts +++ b/src/vs/server/node/remoteExtensionsScanner.ts @@ -79,7 +79,13 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return this._whenExtensionsReady; } - async scanExtensions(language?: string, profileLocation?: URI, extensionDevelopmentLocations?: URI[], languagePackId?: string): Promise { + async scanExtensions( + language?: string, + profileLocation?: URI, + workspaceExtensionLocations?: URI[], + extensionDevelopmentLocations?: URI[], + languagePackId?: string + ): Promise { performance.mark('code/server/willScanExtensions'); this._logService.trace(`Scanning extensions using UI language: ${language}`); @@ -88,7 +94,7 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS const extensionDevelopmentPaths = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined; profileLocation = profileLocation ?? this._userDataProfilesService.defaultProfile.extensionsResource; - const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, extensionDevelopmentPaths, languagePackId); + const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, workspaceExtensionLocations, extensionDevelopmentPaths, languagePackId); this._logService.trace('Scanned Extensions', extensions); this._massageWhenConditions(extensions); @@ -117,16 +123,17 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return extension; } - private async _scanExtensions(profileLocation: URI, language: string, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { + private async _scanExtensions(profileLocation: URI, language: string, workspaceInstalledExtensionLocations: URI[] | undefined, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { await this._ensureLanguagePackIsInstalled(language, languagePackId); - const [builtinExtensions, installedExtensions, developedExtensions] = await Promise.all([ + const [builtinExtensions, installedExtensions, workspaceInstalledExtensions, developedExtensions] = await Promise.all([ this._scanBuiltinExtensions(language), this._scanInstalledExtensions(profileLocation, language), + this._scanWorkspaceInstalledExtensions(language, workspaceInstalledExtensionLocations), this._scanDevelopedExtensions(language, extensionDevelopmentPath) ]); - return dedupExtensions(builtinExtensions, installedExtensions, developedExtensions, this._logService); + return dedupExtensions(builtinExtensions, [...installedExtensions, ...workspaceInstalledExtensions], developedExtensions, this._logService); } private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise { @@ -138,6 +145,19 @@ export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerS return []; } + private async _scanWorkspaceInstalledExtensions(language: string, workspaceInstalledExtensions?: URI[]): Promise { + const result: IExtensionDescription[] = []; + if (workspaceInstalledExtensions?.length) { + const scannedExtensions = await Promise.all(workspaceInstalledExtensions.map(location => this._extensionsScannerService.scanExistingExtension(location, ExtensionType.User, { language }))); + for (const scannedExtension of scannedExtensions) { + if (scannedExtension) { + result.push(toExtensionDescription(scannedExtension, false)); + } + } + } + return result; + } + private async _scanBuiltinExtensions(language: string): Promise { const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true }); return scannedExtensions.map(e => toExtensionDescription(e, false)); @@ -319,9 +339,16 @@ export class RemoteExtensionsScannerChannel implements IServerChannel { case 'scanExtensions': { const language = args[0]; const profileLocation = args[1] ? URI.revive(uriTransformer.transformIncoming(args[1])) : undefined; - const extensionDevelopmentPath = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; - const languagePackId: string | undefined = args[3]; - const extensions = await this.service.scanExtensions(language, profileLocation, extensionDevelopmentPath, languagePackId); + const workspaceExtensionLocations = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; + const extensionDevelopmentPath = Array.isArray(args[3]) ? args[3].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; + const languagePackId: string | undefined = args[4]; + const extensions = await this.service.scanExtensions( + language, + profileLocation, + workspaceExtensionLocations, + extensionDevelopmentPath, + languagePackId + ); return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer)); } case 'scanSingleExtension': { diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 07b71d03c94..46f2f004655 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -9,7 +9,7 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { IURITransformer } from 'vs/base/common/uriIpc'; -import { getMachineId, getSqmMachineId } from 'vs/base/node/id'; +import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { Promises } from 'vs/base/node/pfs'; import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; @@ -132,11 +132,12 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel('userDataProfiles', new RemoteUserDataProfilesServiceChannel(userDataProfilesService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); // Initialize - const [, , machineId, sqmId] = await Promise.all([ + const [, , machineId, sqmId, devDeviceId] = await Promise.all([ configurationService.initialize(), userDataProfilesService.init(), getMachineId(logService.error.bind(logService)), - getSqmMachineId(logService.error.bind(logService)) + getSqmMachineId(logService.error.bind(logService)), + getdevDeviceId(logService.error.bind(logService)) ]); const extensionHostStatusService = new ExtensionHostStatusService(); @@ -156,7 +157,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const config: ITelemetryServiceConfig = { appenders: [oneDsAppender], - commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, sqmId, isInternal, 'remoteAgent'), + commonProperties: resolveCommonProperties(release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, sqmId, devDeviceId, isInternal, 'remoteAgent'), piiPaths: getPiiPathsFromEnvironment(environmentService) }; const initialTelemetryLevelArg = environmentService.args['telemetry-level']; diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index f0c43c66aef..6dc550b6cf0 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -30,13 +30,13 @@ import { isString } from 'vs/base/common/types'; import { CharCode } from 'vs/base/common/charCode'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -const textMimeType = { +const textMimeType: { [ext: string]: string | undefined } = { '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.svg': 'image/svg+xml', -} as { [ext: string]: string | undefined }; +}; /** * Return an error to the client. @@ -306,17 +306,17 @@ export class WebClientServer { scopes: [['user:email'], ['repo']] } : undefined; - const productConfiguration = >{ + const productConfiguration = { embedderIdentifier: 'server-distro', - extensionsGallery: this._webExtensionResourceUrlTemplate ? { + extensionsGallery: this._webExtensionResourceUrlTemplate && this._productService.extensionsGallery ? { ...this._productService.extensionsGallery, - 'resourceUrlTemplate': this._webExtensionResourceUrlTemplate.with({ + resourceUrlTemplate: this._webExtensionResourceUrlTemplate.with({ scheme: 'http', authority: remoteAuthority, path: `${this._webExtensionRoute}/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` }).toString(true) } : undefined - }; + } satisfies Partial; if (!this._environmentService.isBuilt) { try { diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index b3ebdd940c3..3719825841a 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 } from 'vs/workbench/services/authentication/common/authentication'; +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 { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -117,10 +117,17 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu $removeSession(providerId: string, sessionId: string): Promise { return this.authenticationService.removeSession(providerId, sessionId); } - private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { - const message = recreatingSession - ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, providerName) - : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName); + private async loginPrompt(provider: IAuthenticationProvider, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { + let message: string; + + // An internal provider is a special case which is for model access only. + if (provider.id.startsWith(INTERNAL_MODEL_AUTH_PROVIDER_PREFIX)) { + message = nls.localize('confirmModelAccess', "The extension '{0}' wants to access the language models provided by {1}.", extensionName, provider.label); + } else { + message = recreatingSession + ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, provider.label) + : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, provider.label); + } const buttons: IPromptButton[] = [ { @@ -134,7 +141,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu buttons.push({ label: nls.localize('learnMore', "Learn more"), run: async () => { - const result = this.loginPrompt(providerName, extensionName, recreatingSession, options); + const result = this.loginPrompt(provider, extensionName, recreatingSession, options); await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true }); return await result; } @@ -199,7 +206,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions // that we will be "forcing through". const recreatingSession = !!(options.forceNewSession && sessions.length); - const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, uiOptions); + const isAllowed = await this.loginPrompt(provider, extensionName, recreatingSession, uiOptions); if (!isAllowed) { throw new Error('User did not consent to login.'); } diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 99bf1033707..0d63847dae5 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -3,10 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, DisposableMap, IDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -17,14 +21,14 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { ChatAgentLocation, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatContentReference, IChatFollowup, IChatProgress, IChatService, IChatTask, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -35,15 +39,49 @@ interface AgentData { hasFollowups?: boolean; } +class MainThreadChatTask implements IChatTask { + public readonly kind = 'progressTask'; + + public readonly deferred = new DeferredPromise(); + + private readonly _onDidAddProgress = new Emitter(); + public get onDidAddProgress(): Event { return this._onDidAddProgress.event; } + + public readonly progress: (IChatWarningMessage | IChatContentReference)[] = []; + + constructor(public content: IMarkdownString) { } + + task() { + return this.deferred.p; + } + + isSettled() { + return this.deferred.isSettled; + } + + complete(v: string | void) { + this.deferred.complete(v); + } + + add(progress: IChatWarningMessage | IChatContentReference): void { + this.progress.push(progress); + this._onDidAddProgress.fire(progress); + } +} + @extHostNamedCustomer(MainContext.MainThreadChatAgents2) export class MainThreadChatAgents2 extends Disposable implements MainThreadChatAgentsShape2 { private readonly _agents = this._register(new DisposableMap()); private readonly _agentCompletionProviders = this._register(new DisposableMap()); + private readonly _agentIdsToCompletionProviders = this._register(new DisposableMap); private readonly _pendingProgress = new Map void>(); private readonly _proxy: ExtHostChatAgentsShape2; + private _responsePartHandlePool = 0; + private readonly _activeTasks = new Map(); + constructor( extHostContext: IExtHostContext, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @@ -92,7 +130,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._chatService.transferChatSession({ sessionId, inputValue }, URI.revive(toWorkspace)); } - $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string; publisherDisplayName: string } | undefined): void { + $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): void { const staticAgentRegistration = this._chatAgentService.getAgent(id); if (!staticAgentRegistration && !dynamicProps) { if (this._chatAgentService.getAgentsByName(id).length) { @@ -134,12 +172,13 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA disposable = this._chatAgentService.registerDynamicAgent( { id, - name: dynamicProps.name, + name: dynamicProps.name ?? '', // This case is for an API change and can be removed tomorrow description: dynamicProps.description, extensionId: extension, extensionDisplayName: extensionDescription?.displayName ?? extension.value, - extensionPublisherId: '', - publisherDisplayName: dynamicProps.publisherDisplayName, + extensionPublisherId: extensionDescription?.publisher ?? '', + publisherDisplayName: dynamicProps.publisherName, + fullName: dynamicProps.fullName, metadata: revive(metadata), slashCommands: [], locations: [ChatAgentLocation.Panel] // TODO all dynamic participants are panel only? @@ -166,12 +205,43 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._chatAgentService.updateAgent(data.id, revive(metadataUpdate)); } - async $handleProgressChunk(requestId: string, progress: IChatProgressDto): Promise { - const revivedProgress = revive(progress); - this._pendingProgress.get(requestId)?.(revivedProgress as IChatProgress); + async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise { + const revivedProgress = revive(progress) as IChatProgress; + if (revivedProgress.kind === 'progressTask') { + const handle = ++this._responsePartHandlePool; + const responsePartId = `${requestId}_${handle}`; + const task = new MainThreadChatTask(revivedProgress.content); + this._activeTasks.set(responsePartId, task); + this._pendingProgress.get(requestId)?.(task); + return handle; + } else if (responsePartHandle !== undefined) { + const responsePartId = `${requestId}_${responsePartHandle}`; + const task = this._activeTasks.get(responsePartId); + switch (revivedProgress.kind) { + case 'progressTaskResult': + if (task && revivedProgress.content) { + task.complete(revivedProgress.content.value); + this._activeTasks.delete(responsePartId); + } else { + task?.complete(undefined); + } + return responsePartHandle; + case 'warning': + case 'reference': + task?.add(revivedProgress); + return; + } + } + this._pendingProgress.get(requestId)?.(revivedProgress); } - $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void { + $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void { + const provide = async (query: string, token: CancellationToken) => { + const completions = await this._proxy.$invokeCompletionProvider(handle, query, token); + return completions.map((c) => ({ ...c, icon: c.icon ? ThemeIcon.fromId(c.icon) : undefined })); + }; + this._agentIdsToCompletionProviders.set(id, this._chatAgentService.registerAgentCompletionProvider(id, provide)); + this._agentCompletionProviders.set(handle, this._languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'chatAgentCompletions:' + handle, triggerCharacters, @@ -201,7 +271,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA return null; } - const result = await this._proxy.$invokeCompletionProvider(handle, query, token); + const result = await provide(query, token); const variableItems = result.map(v => { const insertText = v.insertText ?? (typeof v.label === 'string' ? v.label : v.label.label); const rangeAfterInsert = new Range(range.insert.startLineNumber, range.insert.startColumn, range.insert.endLineNumber, range.insert.startColumn + insertText.length); @@ -212,7 +282,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA kind: CompletionItemKind.Text, detail: v.detail, documentation: v.documentation, - command: { id: AddDynamicVariableAction.ID, title: '', arguments: [{ widget, range: rangeAfterInsert, variableData: revive(v.value) as any, command: v.command } satisfies IAddDynamicVariableContext] } + command: { id: AddDynamicVariableAction.ID, title: '', arguments: [{ id: v.id, widget, range: rangeAfterInsert, variableData: revive(v.value) as any, command: v.command } satisfies IAddDynamicVariableContext] } } satisfies CompletionItem; }); @@ -223,8 +293,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA })); } - $unregisterAgentCompletionsProvider(handle: number): void { + $unregisterAgentCompletionsProvider(handle: number, id: string): void { this._agentCompletionProviders.deleteAndDispose(handle); + this._agentIdsToCompletionProviders.deleteAndDispose(id); } } diff --git a/src/vs/workbench/api/browser/mainThreadChatVariables.ts b/src/vs/workbench/api/browser/mainThreadChatVariables.ts index 9e08e5d1423..bf7103206a0 100644 --- a/src/vs/workbench/api/browser/mainThreadChatVariables.ts +++ b/src/vs/workbench/api/browser/mainThreadChatVariables.ts @@ -5,7 +5,10 @@ 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'; @@ -47,4 +50,8 @@ 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 9d97e583ba1..d5e11a86dc1 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -180,7 +180,8 @@ export class MainThreadCommentThread implements languages.CommentThread { public resource: string, private _range: T | undefined, private _canReply: boolean, - private _isTemplate: boolean + private _isTemplate: boolean, + public editorId?: string ) { this._isDisposed = false; if (_isTemplate) { @@ -291,7 +292,8 @@ export class MainThreadCommentController implements ICommentController { threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, - isTemplate: boolean + isTemplate: boolean, + editorId?: string ): languages.CommentThread { const thread = new MainThreadCommentThread( commentThreadHandle, @@ -301,7 +303,8 @@ export class MainThreadCommentController implements ICommentController { URI.revive(resource).toString(), range, true, - isTemplate + isTemplate, + editorId ); this._threads.set(commentThreadHandle, thread); @@ -479,8 +482,8 @@ export class MainThreadCommentController implements ICommentController { return ret; } - createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise { - return this._proxy.$createCommentThreadTemplate(this.handle, resource, range); + createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined, editorId?: string): Promise { + return this._proxy.$createCommentThreadTemplate(this.handle, resource, range, editorId); } async updateCommentThreadTemplate(threadHandle: number, range: IRange) { @@ -580,7 +583,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments resource: UriComponents, range: IRange | ICellRange | undefined, extensionId: ExtensionIdentifier, - isTemplate: boolean + isTemplate: boolean, + editorId?: string ): languages.CommentThread | undefined { const provider = this._commentControllers.get(handle); @@ -588,7 +592,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments return undefined; } - return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, isTemplate); + return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range, isTemplate, editorId); } $updateCommentThread(handle: number, diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index aea84bf4413..f58ba4c47fb 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -19,6 +19,7 @@ import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { Event } from 'vs/base/common/event'; +import { isDefined } from 'vs/base/common/types'; @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory { @@ -210,21 +211,26 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb for (const dto of DTOs) { if (dto.type === 'sourceMulti') { - const rawbps = dto.lines.map(l => - { - id: l.id, - enabled: l.enabled, - lineNumber: l.line + 1, - column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784 - condition: l.condition, - hitCondition: l.hitCondition, - logMessage: l.logMessage, - mode: l.mode, - } - ); + const rawbps = dto.lines.map((l): IBreakpointData => ({ + id: l.id, + enabled: l.enabled, + lineNumber: l.line + 1, + column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784 + condition: l.condition, + hitCondition: l.hitCondition, + logMessage: l.logMessage, + mode: l.mode, + })); this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps); } else if (dto.type === 'function') { - this.debugService.addFunctionBreakpoint(dto.functionName, dto.id, dto.mode); + this.debugService.addFunctionBreakpoint({ + name: dto.functionName, + mode: dto.mode, + condition: dto.condition, + hitCondition: dto.hitCondition, + enabled: dto.enabled, + logMessage: dto.logMessage + }, dto.id); } else if (dto.type === 'data') { this.debugService.addDataBreakpoint({ description: dto.label, @@ -248,7 +254,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $registerDebugConfigurationProvider(debugType: string, providerTriggerKind: DebugConfigurationProviderTriggerKind, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, handle: number): Promise { - const provider = { + const provider: IDebugConfigurationProvider = { type: debugType, triggerKind: providerTriggerKind }; @@ -283,7 +289,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $registerDebugAdapterDescriptorFactory(debugType: string, handle: number): Promise { - const provider = { + const provider: IDebugAdapterDescriptorFactory = { type: debugType, createDebugAdapterDescriptor: session => { return Promise.resolve(this._proxy.$provideDebugAdapter(handle, this.getSessionDto(session))); @@ -435,8 +441,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb private convertToDto(bps: (ReadonlyArray)): Array { return bps.map(bp => { if ('name' in bp) { - const fbp = bp; - return { + const fbp: IFunctionBreakpoint = bp; + return { type: 'function', id: fbp.getId(), enabled: fbp.enabled, @@ -444,9 +450,9 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb hitCondition: fbp.hitCondition, logMessage: fbp.logMessage, functionName: fbp.name - }; + } satisfies IFunctionBreakpointDto; } else if ('src' in bp) { - const dbp = bp; + const dbp: IDataBreakpoint = bp; return { type: 'data', id: dbp.getId(), @@ -459,9 +465,9 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb label: dbp.description, canPersist: dbp.canPersist } satisfies IDataBreakpointDto; - } else { - const sbp = bp; - return { + } else if ('uri' in bp) { + const sbp: IBreakpoint = bp; + return { type: 'source', id: sbp.getId(), enabled: sbp.enabled, @@ -471,9 +477,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb uri: sbp.uri, line: sbp.lineNumber > 0 ? sbp.lineNumber - 1 : 0, character: (typeof sbp.column === 'number' && sbp.column > 0) ? sbp.column - 1 : 0, - }; + } satisfies ISourceBreakpointDto; + } else { + return undefined; } - }); + }).filter(isDefined); } } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 0253cba2d4e..a75f2154c9d 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -260,7 +260,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread provideHover: async (model: ITextModel, position: EditorPosition, token: CancellationToken, context?: languages.HoverContext): Promise => { const serializedContext: languages.HoverContext<{ id: number }> = { verbosityRequest: context?.verbosityRequest ? { - action: context.verbosityRequest.action, + verbosityDelta: context.verbosityRequest.verbosityDelta, previousHover: { id: context.verbosityRequest.previousHover.id } } : undefined, }; @@ -367,9 +367,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread })); } - // --- quick fix + // --- code actions - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void { + $registerCodeActionSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, extensionId: string, supportsResolve: boolean): void { const provider: languages.CodeActionProvider = { provideCodeActions: async (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: languages.CodeActionContext, token: CancellationToken): Promise => { const listDto = await this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token); @@ -387,7 +387,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }, providedCodeActionKinds: metadata.providedKinds, documentation: metadata.documentation, - displayName + displayName, + extensionId, }; if (supportsResolve) { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index fcce3b5456f..9b14928a514 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.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 { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -13,7 +12,7 @@ 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 } from 'vs/workbench/contrib/chat/common/languageModels'; +import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage, ILanguageModelChatSelector } 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 { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -37,9 +36,8 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { @IExtensionService private readonly _extensionService: IExtensionService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider); - - this._proxy.$updateLanguageModels({ added: coalesce(_chatProviderService.getLanguageModelIds().map(id => _chatProviderService.lookupLanguageModel(id))) }); - this._store.add(_chatProviderService.onDidChangeLanguageModels(this._proxy.$updateLanguageModels, this._proxy)); + this._proxy.$acceptChatModelMetadata({ added: _chatProviderService.getLanguageModelIds().map(id => ({ identifier: id, metadata: _chatProviderService.lookupLanguageModel(id)! })) }); + this._store.add(_chatProviderService.onDidChangeLanguageModels(this._proxy.$acceptChatModelMetadata, this._proxy)); } dispose(): void { @@ -78,25 +76,12 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { this._providerRegistrations.deleteAndDispose(handle); } - $whenLanguageModelChatRequestMade(identifier: string, extensionId: ExtensionIdentifier, participant?: string | undefined, tokenCount?: number | undefined): void { - this._languageModelStatsService.update(identifier, extensionId, participant, tokenCount); + $selectChatModels(selector: ILanguageModelChatSelector): Promise { + return this._chatProviderService.selectLanguageModels(selector); } - async $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): Promise { - - const activate = this._extensionService.activateByEvent(`onLanguageModelAccess:${providerId}`); - const metadata = this._chatProviderService.lookupLanguageModel(providerId); - - if (metadata) { - return metadata; - } - - await Promise.race([ - activate, - Event.toPromise(Event.filter(this._chatProviderService.onDidChangeLanguageModels, e => Boolean(e.added?.some(value => value.identifier === providerId)))) - ]); - - return this._chatProviderService.lookupLanguageModel(providerId); + $whenLanguageModelChatRequestMade(identifier: string, extensionId: ExtensionIdentifier, participant?: string | undefined, tokenCount?: number | undefined): void { + this._languageModelStatsService.update(identifier, extensionId, participant, tokenCount); } async $fetchResponse(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 8f2f658e810..348cd234e76 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -5,7 +5,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, DisposableStore, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; +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 { Command } from 'vs/editor/common/languages'; @@ -20,6 +20,11 @@ import { ResourceTree } from 'vs/base/common/resourceTree'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { basename } from 'vs/base/common/resources'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +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'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -34,6 +39,25 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } } +class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { + constructor( + textModelService: ITextModelService, + private readonly modelService: IModelService, + private readonly languageService: ILanguageService, + ) { + super(); + this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeSourceControl, this)); + } + + async provideTextContent(resource: URI): Promise { + const existing = this.modelService.getModel(resource); + if (existing) { + return existing; + } + return this.modelService.createModel('', this.languageService.createById('scminput'), resource); + } +} + class MainThreadSCMResourceGroup implements ISCMResourceGroup { readonly resources: ISCMResource[] = []; @@ -197,7 +221,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { get handle(): number { return this._handle; } get label(): string { return this._label; } get rootUri(): URI | undefined { return this._rootUri; } - get inputBoxDocumentUri(): URI { return this._inputBoxDocumentUri; } + get inputBoxTextModel(): ITextModel { return this._inputBoxTextModel; } get contextValue(): string { return this._providerId; } get commitTemplate(): string { return this.features.commitTemplate || ''; } @@ -233,7 +257,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _providerId: string, private readonly _label: string, private readonly _rootUri: URI | undefined, - private readonly _inputBoxDocumentUri: URI, + private readonly _inputBoxTextModel: ITextModel, private readonly _quickDiffService: IQuickDiffService, private readonly _uriIdentService: IUriIdentityService, private readonly _workspaceContextService: IWorkspaceContextService @@ -425,11 +449,16 @@ export class MainThreadSCM implements MainThreadSCMShape { extHostContext: IExtHostContext, @ISCMService private readonly scmService: ISCMService, @ISCMViewService private readonly scmViewService: ISCMViewService, + @ILanguageService private readonly languageService: ILanguageService, + @IModelService private readonly modelService: IModelService, + @ITextModelService private readonly textModelService: ITextModelService, @IQuickDiffService private readonly quickDiffService: IQuickDiffService, @IUriIdentityService private readonly _uriIdentService: IUriIdentityService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSCM); + + this._disposables.add(new SCMInputBoxContentProvider(this.textModelService, this.modelService, this.languageService)); } dispose(): void { @@ -442,12 +471,16 @@ export class MainThreadSCM implements MainThreadSCMShape { this._disposables.dispose(); } - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, URI.revive(inputBoxDocumentUri), this.quickDiffService, this._uriIdentService, this.workspaceContextService); + 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)); + + 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); const disposable = combinedDisposable( + inputBoxTextModelRef, Event.filter(this.scmViewService.onDidFocusRepository, r => r === repository)(_ => this._proxy.$setSelectedSourceControl(handle)), repository.input.onDidChange(({ value }) => this._proxy.$onInputBoxValueChange(handle, value)) ); diff --git a/src/vs/workbench/api/browser/mainThreadSpeech.ts b/src/vs/workbench/api/browser/mainThreadSpeech.ts index fcb28dbc417..6dbb9033772 100644 --- a/src/vs/workbench/api/browser/mainThreadSpeech.ts +++ b/src/vs/workbench/api/browser/mainThreadSpeech.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { raceCancellation } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostContext, ExtHostSpeechShape, MainContext, MainThreadSpeechShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechService, ISpeechToTextEvent, ITextToSpeechEvent } from 'vs/workbench/contrib/speech/common/speechService'; +import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechService, ISpeechToTextEvent, ITextToSpeechEvent, TextToSpeechStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; type SpeechToTextSession = { @@ -16,7 +17,6 @@ type SpeechToTextSession = { type TextToSpeechSession = { readonly onDidChange: Emitter; - synthesize(text: string): Promise; }; type KeywordRecognitionSession = { @@ -72,7 +72,7 @@ export class MainThreadSpeech implements MainThreadSpeechShape { onDidChange: onDidChange.event }; }, - createTextToSpeechSession: (token) => { + createTextToSpeechSession: (token, options) => { if (token.isCancellationRequested) { return { onDidChange: Event.None, @@ -83,13 +83,10 @@ export class MainThreadSpeech implements MainThreadSpeechShape { const disposables = new DisposableStore(); const session = Math.random(); - this.proxy.$createTextToSpeechSession(handle, session); + this.proxy.$createTextToSpeechSession(handle, session, options?.language); const onDidChange = disposables.add(new Emitter()); - this.textToSpeechSessions.set(session, { - onDidChange, - synthesize: text => this.proxy.$synthesizeSpeech(session, text) - }); + this.textToSpeechSessions.set(session, { onDidChange }); disposables.add(token.onCancellationRequested(() => { this.proxy.$cancelTextToSpeechSession(session); @@ -99,7 +96,10 @@ export class MainThreadSpeech implements MainThreadSpeechShape { return { onDidChange: onDidChange.event, - synthesize: text => this.proxy.$synthesizeSpeech(session, text) + synthesize: async text => { + await this.proxy.$synthesizeSpeech(session, text); + await raceCancellation(Event.toPromise(Event.filter(onDidChange.event, e => e.status === TextToSpeechStatus.Stopped)), token); + } }; }, createKeywordRecognitionSession: token => { diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index a316bec0eb3..a3641b6687a 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -18,13 +18,13 @@ import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService'; +import { IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadTesting) -export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider { +export class MainThreadTesting extends Disposable implements MainThreadTestingShape { private readonly proxy: ExtHostTestingShape; private readonly diffListener = this._register(new MutableDisposable()); private readonly testProviderRegistrations = new Map this.proxy.$provideTestFollowups(req, token), + executeTestFollowup: id => this.proxy.$executeTestFollowup(id), + disposeTestFollowups: ids => this.proxy.$disposeTestFollowups(ids), + })); + this._register(this.testService.onDidCancelTestRun(({ runId }) => { this.proxy.$cancelExtensionTestRun(runId); })); @@ -143,7 +149,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh transaction(tx => { let value = task.coverage.read(undefined); if (!value) { - value = new TestCoverage(taskId, this.uriIdentityService, { + value = new TestCoverage(run, taskId, this.uriIdentityService, { getCoverageDetails: (id, token) => this.proxy.$getCoverageDetails(id, token) .then(r => r.map(CoverageDetails.deserialize)), }); diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index bab9d8b97e5..b28bd289ca6 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -11,14 +11,14 @@ import { localize } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier, ExtensionIdentifierSet, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { ThemeIcon } from 'vs/base/common/themables'; import { Extensions as ViewletExtensions, PaneCompositeRegistry } from 'vs/workbench/browser/panecomposite'; -import { CustomTreeView, RawCustomTreeViewContextKey, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; -import { Extensions as ViewContainerExtensions, ICustomViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ResolvableTreeItem, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, ICustomViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; @@ -26,14 +26,6 @@ import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm'; import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions as ExtensionFeaturesRegistryExtensions } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -173,7 +165,7 @@ const viewDescriptor: IJSONSchema = { }, accessibilityHelpContent: { type: 'string', - markdownDescription: localize('vscode.extension.contributes.view.accessibilityHelpContent', "When the accessibility help dialog is invoked in this view, this content will be presented to the user as a markdown string. Keybindings will be resolved when provided in the format of . If there is no keybinding, that will be indicated with a link to configure one.") + markdownDescription: localize('vscode.extension.contributes.view.accessibilityHelpContent', "When the accessibility help dialog is invoked in this view, this content will be presented to the user as a markdown string. Keybindings will be resolved when provided in the format of . If there is no keybinding, that will be indicated and this command will be included in a quickpick for easy configuration.") } } }; @@ -291,53 +283,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { this.viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); this.handleAndRegisterCustomViewContainers(); this.handleAndRegisterCustomViews(); - - // Abstract tree has it's own implementation of triggering custom hover - // TreeView uses it's own implementation due to setting focus inside the (markdown) - let showTreeHoverCancellation = new CancellationTokenSource(); - KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'workbench.action.showTreeHover', - handler: async (accessor: ServicesAccessor, ...args: any[]) => { - showTreeHoverCancellation.cancel(); - showTreeHoverCancellation = new CancellationTokenSource(); - const listService = accessor.get(IListService); - const treeViewsService = accessor.get(ITreeViewsService); - const hoverService = accessor.get(IHoverService); - const lastFocusedList = listService.lastFocusedList; - if (!(lastFocusedList instanceof AsyncDataTree)) { - return; - } - const focus = lastFocusedList.getFocus(); - if (!focus || (focus.length === 0)) { - return; - } - const treeItem = focus[0]; - - if (treeItem instanceof ResolvableTreeItem) { - await treeItem.resolve(showTreeHoverCancellation.token); - } - if (!treeItem.tooltip) { - return; - } - const element = treeViewsService.getRenderedTreeElement(('handle' in treeItem) ? treeItem.handle : treeItem); - if (!element) { - return; - } - hoverService.showHover({ - content: treeItem.tooltip, - target: element, - position: { - hoverPosition: HoverPosition.BELOW, - }, - persistence: { - hideOnHover: false - } - }, true); - }, - weight: KeybindingWeight.WorkbenchContrib + 1, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyI), - when: ContextKeyExpr.and(RawCustomTreeViewContextKey, WorkbenchListFocusContextKey) - }); } private handleAndRegisterCustomViewContainers() { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 4070489c54e..680d04cb5dd 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable } from 'vs/base/common/lifecycle'; @@ -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 { ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, 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'; @@ -178,7 +178,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch, extHostLogService)); const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, extHostNotebook)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); @@ -209,7 +209,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I 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)); + const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, initData.quality)); const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); @@ -453,6 +453,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'testObserver'); return extHostTesting.runTests(provider); }, + registerTestFollowupProvider(provider) { + checkProposedApiEnabled(extension, 'testObserver'); + return extHostTesting.registerTestFollowupProvider(provider); + }, get onDidChangeTestResults() { checkProposedApiEnabled(extension, 'testObserver'); return _asExtensionEvent(extHostTesting.onResultsChanged); @@ -1244,9 +1248,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostDebugService.breakpoints; }, get activeStackItem() { - if (!isProposedApiEnabled(extension, 'debugFocus')) { - return undefined; - } return extHostDebugService.activeStackItem; }, registerDebugVisualizationProvider(id, provider) { @@ -1273,7 +1274,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return _asExtensionEvent(extHostDebugService.onDidChangeBreakpoints)(listener, thisArgs, disposables); }, onDidChangeActiveStackItem(listener, thisArg?, disposables?) { - checkProposedApiEnabled(extension, 'debugFocus'); return _asExtensionEvent(extHostDebugService.onDidChangeActiveStackItem)(listener, thisArg, disposables); }, registerDebugConfigurationProvider(debugType: string, provider: vscode.DebugConfigurationProvider, triggerKind?: vscode.DebugConfigurationProviderTriggerKind) { @@ -1381,8 +1381,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: interactive const interactive: typeof vscode.interactive = { - // IMPORTANT - // this needs to be updated whenever the API proposal changes + // TODO Can be deleted after another Insiders _version: 1, transferActiveChat(toWorkspace: vscode.Uri) { checkProposedApiEnabled(extension, 'interactive'); @@ -1408,53 +1407,51 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: chat const chat: typeof vscode.chat = { + // IMPORTANT + // this needs to be updated whenever the API proposal changes and breaks backwards compatibility + _version: 1, + registerChatResponseProvider(id: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata) { checkProposedApiEnabled(extension, 'chatProvider'); return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); }, - registerChatVariableResolver(name: string, description: string, resolver: vscode.ChatVariableResolver) { + registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, icon?: vscode.ThemeIcon) { checkProposedApiEnabled(extension, 'chatVariableResolver'); - return extHostChatVariables.registerVariableResolver(extension, name, description, resolver); + return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver, fullName, icon?.id); }, registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); return extHostLanguageFeatures.registerMappedEditsProvider(extension, selector, provider); }, createChatParticipant(id: string, handler: vscode.ChatExtendedRequestHandler) { - checkProposedApiEnabled(extension, 'chatParticipant'); return extHostChatAgents2.createChatAgent(extension, id, handler); }, - createDynamicChatParticipant(id: string, name: string, publisherName: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { - checkProposedApiEnabled(extension, 'chatParticipantAdditions'); - return extHostChatAgents2.createDynamicChatAgent(extension, id, name, publisherName, description, handler); + createDynamicChatParticipant(id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + 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 = { - get languageModels() { - checkProposedApiEnabled(extension, 'languageModels'); - return extHostLanguageModels.getLanguageModelIds(); + 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 ?? {}); }, - onDidChangeLanguageModels: (listener, thisArgs?, disposables?) => { - checkProposedApiEnabled(extension, 'languageModels'); + 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); }, - sendChatRequest(languageModel: string, messages: (vscode.LanguageModelChatMessage | vscode.LanguageModelChatMessage2)[], options?: vscode.LanguageModelChatRequestOptions, token?: vscode.CancellationToken) { - checkProposedApiEnabled(extension, 'languageModels'); - token ??= CancellationToken.None; - options ??= {}; - return extHostLanguageModels.sendChatRequest(extension, languageModel, messages, options, token); - }, - computeTokenLength(languageModel: string, text: string | vscode.LanguageModelChatMessage, token?: vscode.CancellationToken) { - checkProposedApiEnabled(extension, 'languageModels'); - token ??= CancellationToken.None; - return extHostLanguageModels.computeTokenLength(languageModel, text, token); - }, - getLanguageModelInformation(languageModel: string) { - checkProposedApiEnabled(extension, 'languageModels'); - return extHostLanguageModels.getLanguageModelInfo(languageModel); - }, // --- embeddings get embeddingModels() { checkProposedApiEnabled(extension, 'embeddings'); @@ -1679,7 +1676,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LinkedEditingRanges: extHostTypes.LinkedEditingRanges, TestResultState: extHostTypes.TestResultState, TestRunRequest: extHostTypes.TestRunRequest, - TestRunRequest2: extHostTypes.TestRunRequest, TestMessage: extHostTypes.TestMessage, TestTag: extHostTypes.TestTag, TestRunProfileKind: extHostTypes.TestRunProfileKind, @@ -1688,6 +1684,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I DataTransferItem: extHostTypes.DataTransferItem, TestCoverageCount: extHostTypes.TestCoverageCount, FileCoverage: extHostTypes.FileCoverage, + FileCoverage2: extHostTypes.FileCoverage, StatementCoverage: extHostTypes.StatementCoverage, BranchCoverage: extHostTypes.BranchCoverage, DeclarationCoverage: extHostTypes.DeclarationCoverage, @@ -1723,17 +1720,20 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, ChatResponseAnchorPart: extHostTypes.ChatResponseAnchorPart, ChatResponseProgressPart: extHostTypes.ChatResponseProgressPart, + ChatResponseProgressPart2: extHostTypes.ChatResponseProgressPart2, ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatResponseDetectedParticipantPart: extHostTypes.ChatResponseDetectedParticipantPart, + ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, - LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage2, + 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 diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9f2dd18fb7f..2d10d6b7129 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -6,6 +6,7 @@ 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'; @@ -52,9 +53,9 @@ import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; 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, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; +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 * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -64,7 +65,7 @@ import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkspaceSymbol, NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search'; import { IRawClosedNotebookFileMatch } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { IKeywordRecognitionEvent, ISpeechProviderMetadata, ISpeechToTextEvent, ITextToSpeechEvent } from 'vs/workbench/contrib/speech/common/speechService'; -import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; +import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; 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'; @@ -143,7 +144,7 @@ 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): languages.CommentThread | undefined; + $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange | undefined, 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; @@ -414,7 +415,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerMultiDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerLinkedEditingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; - $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void; + $registerCodeActionSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, extensionID: string, supportsResolve: boolean): void; $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], metadata: IPasteEditProviderMetadataDto): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; $registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string, supportRanges: boolean): void; @@ -1188,7 +1189,7 @@ export interface ExtHostSpeechShape { $createSpeechToTextSession(handle: number, session: number, language?: string): Promise; $cancelSpeechToTextSession(session: number): Promise; - $createTextToSpeechSession(handle: number, session: number): Promise; + $createTextToSpeechSession(handle: number, session: number, language?: string): Promise; $synthesizeSpeech(session: number, text: string): Promise; $cancelTextToSpeechSession(session: number): Promise; @@ -1201,7 +1202,8 @@ export interface MainThreadLanguageModelsShape extends IDisposable { $unregisterProvider(handle: number): void; $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise; - $prepareChatAccess(extension: ExtensionIdentifier, providerId: string, justification?: string): 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; @@ -1209,7 +1211,7 @@ export interface MainThreadLanguageModelsShape extends IDisposable { } export interface ExtHostLanguageModelsShape { - $updateLanguageModels(data: { added?: ILanguageModelChatMetadata[]; removed?: string[] }): void; + $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; @@ -1231,18 +1233,28 @@ export interface IExtensionChatAgentMetadata extends Dto { hasFollowups?: boolean; } +export interface IDynamicChatAgentProps { + name: string; + publisherName: string; + description?: string; + fullName?: string; +} + export interface MainThreadChatAgentsShape2 extends IDisposable { - $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: { name: string; description: string; publisherDisplayName: string } | undefined): void; - $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; - $unregisterAgentCompletionsProvider(handle: number): void; + $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): void; + $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void; + $unregisterAgentCompletionsProvider(handle: number, id: string): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; $unregisterAgent(handle: number): void; - $handleProgressChunk(requestId: string, chunk: IChatProgressDto): Promise; + $handleProgressChunk(requestId: string, chunk: IChatProgressDto, handle?: number): Promise; $transferActiveChatSession(toWorkspace: UriComponents): void; } export interface IChatAgentCompletionItem { + id: string; + fullName?: string; + icon?: string; insertText?: string; label: string | languages.CompletionItemLabel; value: IChatRequestVariableValueDto; @@ -1252,7 +1264,8 @@ export interface IChatAgentCompletionItem { } export type IChatContentProgressDto = - | Dto; + | Dto> + | IChatTaskDto; export type IChatAgentHistoryEntryDto = { request: IChatAgentRequest; @@ -1263,7 +1276,7 @@ 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; - $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void; + $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; $provideWelcomeMessage(handle: number, location: ChatAgentLocation, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined>; @@ -1278,6 +1291,7 @@ 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 type IChatRequestVariableValueDto = Dto; @@ -1321,7 +1335,8 @@ export type IDocumentContextDto = { }; export type IChatProgressDto = - | Dto; + | Dto> + | IChatTaskDto; export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; @@ -1507,7 +1522,7 @@ export interface SCMHistoryItemChangeDto { } export interface MainThreadSCMShape extends IDisposable { - $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void; + $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): Promise; $updateSourceControl(handle: number, features: SCMProviderFeatures): void; $unregisterSourceControl(handle: number): void; @@ -2458,7 +2473,7 @@ export interface ExtHostProgressShape { } export interface ExtHostCommentsShape { - $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined): Promise; + $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined, editorId?: string): Promise; $updateCommentThreadTemplate(commentControllerHandle: number, threadHandle: number, range: IRange): Promise; $deleteCommentThread(commentControllerHandle: number, commentThreadHandle: number): void; $provideCommentingRanges(commentControllerHandle: number, uriComponents: UriComponents, token: CancellationToken): Promise<{ ranges: IRange[]; fileComments: boolean } | undefined>; @@ -2688,8 +2703,6 @@ export interface ExtHostTestingShape { $cancelExtensionTestRun(runId: string | undefined): void; /** Handles a diff of tests, as a result of a subscribeToDiffs() call */ $acceptDiff(diff: TestsDiffOp.Serialized[]): void; - /** Publishes that a test run finished. */ - $publishTestResults(results: ISerializedTestResults[]): void; /** 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. */ @@ -2704,6 +2717,17 @@ export interface ExtHostTestingShape { $syncTests(): Promise; /** Sets the active test run profiles */ $setDefaultRunProfiles(profiles: Record): void; + + // --- test results: + + /** Publishes that a test run finished. */ + $publishTestResults(results: ISerializedTestResults[]): void; + /** Requests followup actions for a test (failure) message */ + $provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise; + /** Actions a followup actions for a test (failure) message */ + $executeTestFollowup(id: number): Promise; + /** Disposes followup actions for a test (failure) message */ + $disposeTestFollowups(id: number[]): void; } export interface ExtHostLocalizationShape { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 9f3631f5a78..435c4c45f69 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -17,12 +17,12 @@ import { URI } from 'vs/base/common/uri'; import { Location } from 'vs/editor/common/languages'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IExtensionChatAgentMetadata, IMainContext, MainContext, MainThreadChatAgentsShape2 } from 'vs/workbench/api/common/extHost.protocol'; +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 * 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, IChatProgress, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatContentReference, IChatFollowup, IChatUserActionEvent, ChatAgentVoteDirection, IChatResponseErrorDetails } 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'; @@ -68,12 +68,36 @@ class ChatAgentResponseStream { } } - const _report = (progress: Dto) => { + const _report = (progress: IChatProgressDto, task?: (progress: vscode.Progress) => Thenable) => { // Measure the time to the first progress update with real markdown content if (typeof this._firstProgress === 'undefined' && 'content' in progress) { this._firstProgress = this._stopWatch.elapsed(); } - this._proxy.$handleProgressChunk(this._request.requestId, progress); + + if (task) { + const progressReporterPromise = this._proxy.$handleProgressChunk(this._request.requestId, progress); + const progressReporter = { + report: (p: vscode.ChatResponseWarningPart | vscode.ChatResponseReferencePart) => { + progressReporterPromise?.then((handle) => { + if (handle) { + if (extHostTypes.MarkdownString.isMarkdownString(p.value)) { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatResponseWarningPart.from(p), handle); + } else { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatResponseReferencePart.from(p), handle); + } + } + }); + } + }; + + Promise.all([progressReporterPromise, task?.(progressReporter)]).then(([handle, res]) => { + if (handle !== undefined && res !== undefined) { + this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.ChatTaskResult.from(res), handle); + } + }); + } else { + this._proxy.$handleProgressChunk(this._request.requestId, progress); + } }; this._apiObject = { @@ -116,11 +140,11 @@ class ChatAgentResponseStream { _report(dto); return this; }, - progress(value) { + progress(value, task?: ((progress: vscode.Progress) => Thenable)) { throwIfDone(this.progress); - const part = new extHostTypes.ChatResponseProgressPart(value); - const dto = typeConvert.ChatResponseProgressPart.from(part); - _report(dto); + const part = new extHostTypes.ChatResponseProgressPart2(value, task); + const dto = task ? typeConvert.ChatTask.from(part) : typeConvert.ChatResponseProgressPart.from(part); + _report(dto, task); return this; }, warning(value) { @@ -134,6 +158,10 @@ class ChatAgentResponseStream { reference(value, iconPath) { throwIfDone(this.reference); + if ('variableName' in value) { + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + } + if ('variableName' in value && !value.value) { // The participant used this variable. Does that variable have any references to pull in? const matchingVarData = that._request.variables.variables.find(v => v.name === value.variableName); @@ -182,6 +210,15 @@ class ChatAgentResponseStream { _report(dto); return this; }, + confirmation(title, message, data) { + throwIfDone(this.confirmation); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseConfirmationPart(title, message, data); + const dto = typeConvert.ChatResponseConfirmationPart.from(part); + _report(dto); + return this; + }, push(part) { throwIfDone(this.push); @@ -189,7 +226,8 @@ class ChatAgentResponseStream { part instanceof extHostTypes.ChatResponseTextEditPart || part instanceof extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart || part instanceof extHostTypes.ChatResponseDetectedParticipantPart || - part instanceof extHostTypes.ChatResponseWarningPart + part instanceof extHostTypes.ChatResponseWarningPart || + part instanceof extHostTypes.ChatResponseConfirmationPart ) { checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); } @@ -225,6 +263,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS mainContext: IMainContext, private readonly _logService: ILogService, private readonly commands: ExtHostCommands, + private readonly quality: string | undefined ) { super(); this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); @@ -236,19 +275,22 @@ 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, id, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); + if (agent.isAgentEnabled()) { + this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); + } + return agent.apiAgent; } - createDynamicChatAgent(extension: IExtensionDescription, id: string, name: string, publisherName: string, description: string, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { + createDynamicChatAgent(extension: IExtensionDescription, id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.ChatExtendedRequestHandler): vscode.ChatParticipant { const handle = ExtHostChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, id, this._proxy, handle, handler); + const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, id, { isSticky: true } satisfies IExtensionChatAgentMetadata, { name, description, publisherDisplayName: publisherName }); + this._proxy.$registerAgent(handle, extension.identifier, id, { isSticky: true } satisfies IExtensionChatAgentMetadata, dynamicProps); return agent.apiAgent; } @@ -285,7 +327,18 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS return { errorDetails: { message: msg }, timings: stream.timings }; } } - return { errorDetails: result?.errorDetails, timings: stream.timings, metadata: result?.metadata }; + let errorDetails: IChatResponseErrorDetails | undefined; + if (result?.errorDetails) { + errorDetails = { + ...result.errorDetails, + responseIsIncomplete: true + }; + } + if (errorDetails?.responseIsRedacted) { + checkProposedApiEnabled(agent.extension, 'chatParticipantPrivate'); + } + + return { errorDetails, timings: stream.timings, metadata: result?.metadata } satisfies IChatAgentResult; }), token); } catch (e) { this._logService.error(e, agent.extension); @@ -344,7 +397,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS .map(f => typeConvert.ChatFollowup.from(f, request)); } - $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void { + $acceptFeedback(handle: number, result: IChatAgentResult, vote: ChatAgentVoteDirection, reportIssue?: boolean): void { const agent = this._agents.get(handle); if (!agent) { return; @@ -353,10 +406,10 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const ehResult = typeConvert.ChatAgentResult.to(result); let kind: extHostTypes.ChatResultFeedbackKind; switch (vote) { - case InteractiveSessionVoteDirection.Down: + case ChatAgentVoteDirection.Down: kind = extHostTypes.ChatResultFeedbackKind.Unhelpful; break; - case InteractiveSessionVoteDirection.Up: + case ChatAgentVoteDirection.Up: kind = extHostTypes.ChatResultFeedbackKind.Helpful; break; } @@ -424,7 +477,6 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS class ExtHostChatAgent { private _followupProvider: vscode.ChatFollowupProvider | undefined; - private _fullName: string | undefined; private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined; private _isDefault: boolean | undefined; private _helpTextPrefix: string | vscode.MarkdownString | undefined; @@ -437,9 +489,11 @@ class ExtHostChatAgent { private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; private _welcomeMessageProvider?: vscode.ChatWelcomeMessageProvider | undefined; private _requester: vscode.ChatRequesterInformation | undefined; + private _supportsSlowReferences: boolean | undefined; constructor( public readonly extension: IExtensionDescription, + private readonly quality: string | undefined, public readonly id: string, private readonly _proxy: MainThreadChatAgentsShape2, private readonly _handle: number, @@ -462,6 +516,11 @@ 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 []; @@ -519,8 +578,11 @@ class ExtHostChatAgent { } updateScheduled = true; queueMicrotask(() => { + if (!that.isAgentEnabled()) { + return; + } + this._proxy.$updateAgent(this._handle, { - fullName: this._fullName, icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : 'light' in this._iconPath ? this._iconPath.light : @@ -535,7 +597,8 @@ class ExtHostChatAgent { helpTextVariablesPrefix: (!this._helpTextVariablesPrefix || typeof this._helpTextVariablesPrefix === 'string') ? this._helpTextVariablesPrefix : typeConvert.MarkdownString.from(this._helpTextVariablesPrefix), helpTextPostfix: (!this._helpTextPostfix || typeof this._helpTextPostfix === 'string') ? this._helpTextPostfix : typeConvert.MarkdownString.from(this._helpTextPostfix), supportIssueReporting: this._supportIssueReporting, - requester: this._requester + requester: this._requester, + supportsSlowVariables: this._supportsSlowReferences, }); updateScheduled = false; }); @@ -546,15 +609,6 @@ class ExtHostChatAgent { get id() { return that.id; }, - get fullName() { - checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - return that._fullName ?? that.extension.displayName ?? that.extension.name; - }, - set fullName(v) { - checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - that._fullName = v; - updateMetadataSoon(); - }, get iconPath() { return that._iconPath; }, @@ -622,11 +676,11 @@ class ExtHostChatAgent { updateMetadataSoon(); }, get supportIssueReporting() { - checkProposedApiEnabled(that.extension, 'chatParticipantAdditions'); + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); return that._supportIssueReporting; }, set supportIssueReporting(v) { - checkProposedApiEnabled(that.extension, 'chatParticipantAdditions'); + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); that._supportIssueReporting = v; updateMetadataSoon(); }, @@ -641,9 +695,9 @@ class ExtHostChatAgent { throw new Error('triggerCharacters are required'); } - that._proxy.$registerAgentCompletionsProvider(that._handle, v.triggerCharacters); + that._proxy.$registerAgentCompletionsProvider(that._handle, that.id, v.triggerCharacters); } else { - that._proxy.$unregisterAgentCompletionsProvider(that._handle); + that._proxy.$unregisterAgentCompletionsProvider(that._handle, that.id); } }, get participantVariableProvider() { @@ -670,6 +724,15 @@ class ExtHostChatAgent { get requester() { return that._requester; }, + set supportsSlowReferences(v) { + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); + that._supportsSlowReferences = v; + updateMetadataSoon(); + }, + get supportsSlowReferences() { + checkProposedApiEnabled(that.extension, 'chatParticipantPrivate'); + return that._supportsSlowReferences; + }, dispose() { disposed = true; that._followupProvider = undefined; diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index c0587e4ef06..5f0bf7d2449 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -6,6 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostChatVariablesShape, IChatVariableResolverProgressDto, IMainContext, MainContext, MainThreadChatVariablesShape } from 'vs/workbench/api/common/extHost.protocol'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; @@ -52,16 +53,21 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { return undefined; } - registerVariableResolver(extension: IExtensionDescription, name: string, description: string, resolver: vscode.ChatVariableResolver): IDisposable { + registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, themeIconId?: string): IDisposable { const handle = ExtHostChatVariables._idPool++; - this._resolver.set(handle, { extension, data: { name, description }, resolver: resolver }); - this._proxy.$registerVariable(handle, { name, description }); + const icon = themeIconId ? ThemeIcon.fromId(themeIconId) : undefined; + this._resolver.set(handle, { extension, data: { id, name, description: userDescription, modelDescription, icon }, resolver: resolver }); + this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription, isSlow, fullName, icon }); return toDisposable(() => { this._resolver.delete(handle); 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 38678540e4a..b3f54666152 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -160,14 +160,14 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return commentController.value; } - async $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined): Promise { + async $createCommentThreadTemplate(commentControllerHandle: number, uriComponents: UriComponents, range: IRange | undefined, editorId?: string): Promise { const commentController = this._commentControllers.get(commentControllerHandle); if (!commentController) { return; } - commentController.$createCommentThreadTemplate(uriComponents, range); + commentController.$createCommentThreadTemplate(uriComponents, range, editorId); } async $setActiveComment(controllerHandle: number, commentInfo: { commentThreadHandle: number; uniqueIdInThread?: number }): Promise { @@ -409,7 +409,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo private _range: vscode.Range | undefined, private _comments: vscode.Comment[], public readonly extensionDescription: IExtensionDescription, - private _isTemplate: boolean + private _isTemplate: boolean, + editorId?: string ) { this._acceptInputDisposables.value = new DisposableStore(); @@ -424,7 +425,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this._uri, extHostTypeConverter.Range.from(this._range), extensionDescription.identifier, - this._isTemplate + this._isTemplate, + editorId ); this._localDisposables = []; @@ -680,8 +682,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } } - $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange | undefined): ExtHostCommentThread { - const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension, true); + $createCommentThreadTemplate(uriComponents: UriComponents, range: IRange | undefined, editorId?: string): ExtHostCommentThread { + const commentThread = new ExtHostCommentThread(this.id, this.handle, undefined, URI.revive(uriComponents), extHostTypeConverter.Range.to(range), [], this._extension, true, editorId); commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; this._threads.set(commentThread.handle, commentThread); return commentThread; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index d39a2dc0d8e..d5ea2bdf817 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -413,11 +413,11 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E if (bp instanceof SourceBreakpoint) { let dto = map.get(bp.location.uri.toString()); if (!dto) { - dto = { + dto = { type: 'sourceMulti', uri: bp.location.uri, lines: [] - }; + } satisfies ISourceMultiBreakpointDto; map.set(bp.location.uri.toString(), dto); dtos.push(dto); } @@ -883,28 +883,28 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E private convertToDto(x: vscode.DebugAdapterDescriptor): Dto { if (x instanceof DebugAdapterExecutable) { - return { + return { type: 'executable', command: x.command, args: x.args, options: x.options - }; + } satisfies IDebugAdapterExecutable; } else if (x instanceof DebugAdapterServer) { - return { + return { type: 'server', port: x.port, host: x.host - }; + } satisfies IDebugAdapterServer; } else if (x instanceof DebugAdapterNamedPipeServer) { - return { + return { type: 'pipeServer', path: x.path - }; + } satisfies IDebugAdapterNamedPipeServer; } else if (x instanceof DebugAdapterInlineImplementation) { - return >{ + return { type: 'implementation', implementation: x.implementation - }; + } as Dto; } else { throw new Error('convertToDto unexpected type'); } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 2cb1dd5226a..215ff5fda37 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -277,7 +277,7 @@ class HoverAdapter { if (!previousHover) { throw new Error(`Hover with id ${previousHoverId} not found`); } - const hoverContext: vscode.HoverContext = { action: context.verbosityRequest.action, previousHover }; + const hoverContext: vscode.HoverContext = { verbosityDelta: context.verbosityRequest.verbosityDelta, previousHover }; value = await this._provider.provideHover(doc, pos, token, hoverContext); } else { value = await this._provider.provideHover(doc, pos, token); @@ -2196,6 +2196,10 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return ext.displayName || ext.name; } + private static _extId(ext: IExtensionDescription): string { + return ext.identifier.value; + } + // --- outline registerDocumentSymbolProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider, metadata?: vscode.DocumentSymbolProviderMetadata): vscode.Disposable { @@ -2385,18 +2389,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, ReferenceAdapter, adapter => adapter.provideReferences(URI.revive(resource), position, context, token), undefined, token); } - // --- quick fix + // --- code actions registerCodeActionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { const store = new DisposableStore(); const handle = this._addNewAdapter(new CodeActionAdapter(this._documents, this._commands.converter, this._diagnostics, provider, this._logService, extension, this._apiDeprecation), extension); - this._proxy.$registerQuickFixSupport(handle, this._transformDocumentSelector(selector, extension), { + this._proxy.$registerCodeActionSupport(handle, this._transformDocumentSelector(selector, extension), { providedKinds: metadata?.providedCodeActionKinds?.map(kind => kind.value), documentation: metadata?.documentation?.map(x => ({ kind: x.kind.value, command: this._commands.converter.toInternal(x.command, store), })) - }, ExtHostLanguageFeatures._extLabel(extension), Boolean(provider.resolveCodeAction)); + }, ExtHostLanguageFeatures._extLabel(extension), ExtHostLanguageFeatures._extId(extension), Boolean(provider.resolveCodeAction)); store.add(this._createDisposable(handle)); return store; } diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index a7823af0e4c..97ee59fa601 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -3,26 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { AsyncIterableSource, Barrier } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationError } 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'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Progress } from 'vs/platform/progress/common/progress'; import { ExtHostLanguageModelsShape, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; +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 type * as vscode from 'vscode'; -import { Progress } from 'vs/platform/progress/common/progress'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; -import { ExtensionIdentifier, ExtensionIdentifierMap, ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { AsyncIterableSource, Barrier } from 'vs/base/common/async'; -import { Emitter, Event } from 'vs/base/common/event'; -import { localize } from 'vs/nls'; import { INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; -import { CancellationError } from 'vs/base/common/errors'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -import { IExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; -import { ILogService } from 'vs/platform/log/common/log'; -import { Iterable } from 'vs/base/common/iterator'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import type * as vscode from 'vscode'; export interface IExtHostLanguageModels extends ExtHostLanguageModels { } @@ -60,7 +60,7 @@ class LanguageModelResponse { const that = this; this.apiObject = { // result: promise, - stream: that._defaultStream.asyncIterable, + text: that._defaultStream.asyncIterable, // streams: AsyncIterable[] // FUTURE responses per N }; } @@ -110,7 +110,6 @@ class LanguageModelResponse { stream.resolve(); } } - } export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { @@ -121,11 +120,11 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { private readonly _proxy: MainThreadLanguageModelsShape; private readonly _onDidChangeModelAccess = new Emitter<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); - private readonly _onDidChangeProviders = new Emitter(); + private readonly _onDidChangeProviders = new Emitter(); readonly onDidChangeProviders = this._onDidChangeProviders.event; private readonly _languageModels = new Map(); - private readonly _allLanguageModelData = new Map(); // these are ALL models, not just the one in this EH + private readonly _allLanguageModelData = new Map }>(); // these are ALL models, not just the one in this EH private readonly _modelAccessList = new ExtensionIdentifierMap(); private readonly _pendingRequest = new Map(); @@ -153,13 +152,17 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { accountLabel: typeof metadata.auth === 'object' ? metadata.auth.label : undefined }; } - this._proxy.$registerLanguageModelProvider(handle, identifier, { + this._proxy.$registerLanguageModelProvider(handle, `${ExtensionIdentifier.toKey(extension.identifier)}/${handle}/${identifier}`, { extension: extension.identifier, - identifier: identifier, + id: identifier, + vendor: metadata.vendor ?? ExtensionIdentifier.toKey(extension.identifier), name: metadata.name ?? '', + family: metadata.family ?? '', version: metadata.version, - tokens: metadata.tokens, - auth + maxInputTokens: metadata.maxInputTokens, + maxOutputTokens: metadata.maxOutputTokens, + auth, + targetExtensions: metadata.extensions }); const responseReceivedListener = provider.onDidReceiveLanguageModelResponse2?.(({ extensionId, participant, tokenCount }) => { @@ -186,11 +189,13 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { this._proxy.$handleProgressChunk(requestId, { index: fragment.index, part: fragment.part }); }); - if (data.provider.provideLanguageModelResponse) { - return data.provider.provideLanguageModelResponse(messages.map(typeConvert.LanguageModelChatMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); - } else { - return data.provider.provideLanguageModelResponse2(messages.map(typeConvert.LanguageModelMessage.to), options, ExtensionIdentifier.toKey(from), progress, token); - } + return data.provider.provideLanguageModelResponse( + messages.map(typeConvert.LanguageModelChatMessage.to), + options, + ExtensionIdentifier.toKey(from), + progress, + token + ); } @@ -207,20 +212,16 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { //#region --- making request - $updateLanguageModels(data: { added?: ILanguageModelChatMetadata[] | undefined; removed?: string[] | undefined }): void { - const added: string[] = []; - const removed: string[] = []; + $acceptChatModelMetadata(data: { added?: { identifier: string; metadata: ILanguageModelChatMetadata }[] | undefined; removed?: string[] | undefined }): void { if (data.added) { - for (const metadata of data.added) { - this._allLanguageModelData.set(metadata.identifier, metadata); - added.push(metadata.identifier); + for (const { identifier, metadata } of data.added) { + this._allLanguageModelData.set(identifier, { metadata, apiObjects: new ExtensionIdentifierMap() }); } } if (data.removed) { for (const id of data.removed) { // clean up this._allLanguageModelData.delete(id); - removed.push(id); // cancel pending requests for this model for (const [key, value] of this._pendingRequest) { @@ -232,44 +233,66 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - this._onDidChangeProviders.fire(Object.freeze({ - added: Object.freeze(added), - removed: Object.freeze(removed) - })); - // TODO@jrieken@TylerLeonhardt - this is a temporary hack to populate the auth providers - data.added?.forEach(this._fakeAuthPopulate, this); + data.added?.forEach(added => this._fakeAuthPopulate(added.metadata)); + + this._onDidChangeProviders.fire(undefined); } - getLanguageModelIds(): string[] { - return Array.from(this._allLanguageModelData.keys()); - } + async selectLanguageModels(extension: IExtensionDescription, selector: vscode.LanguageModelChatSelector) { - $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void { - const updated = new Array<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); - for (const { from, to, enabled } of data) { - const set = this._modelAccessList.get(from) ?? new ExtensionIdentifierSet(); - const oldValue = set.has(to); - if (oldValue !== enabled) { - if (enabled) { - set.add(to); - } else { - set.delete(to); - } - this._modelAccessList.set(from, set); - const newItem = { from, to }; - updated.push(newItem); - this._onDidChangeModelAccess.fire(newItem); + // this triggers extension activation + const models = await this._proxy.$selectChatModels({ ...selector, extension: extension.identifier }); + + const result: vscode.LanguageModelChat[] = []; + const that = this; + for (const identifier of models) { + const data = this._allLanguageModelData.get(identifier); + if (!data) { + // model gone? is this an error on us? + continue; } + + let apiObject = data.apiObjects.get(extension.identifier); + + if (!apiObject) { + apiObject = { + id: identifier, + vendor: data.metadata.vendor, + family: data.metadata.family, + version: data.metadata.version, + name: data.metadata.name, + maxInputTokens: data.metadata.maxInputTokens, + countTokens(text, token) { + if (!that._allLanguageModelData.has(identifier)) { + throw extHostTypes.LanguageModelError.NotFound(identifier); + } + return that._computeTokenLength(identifier, text, token ?? CancellationToken.None); + }, + sendRequest(messages, options, token) { + if (!that._allLanguageModelData.has(identifier)) { + throw extHostTypes.LanguageModelError.NotFound(identifier); + } + return that._sendChatRequest(extension, identifier, messages, options ?? {}, token ?? CancellationToken.None); + } + }; + + Object.freeze(apiObject); + data.apiObjects.set(extension.identifier, apiObject); + } + + result.push(apiObject); } + + return result; } - async sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: (vscode.LanguageModelChatMessage | vscode.LanguageModelChatMessage2)[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { + private async _sendChatRequest(extension: IExtensionDescription, languageModelId: string, messages: vscode.LanguageModelChatMessage[], options: vscode.LanguageModelChatRequestOptions, token: CancellationToken) { const internalMessages: IChatMessage[] = this._convertMessages(extension, messages); const from = extension.identifier; - const metadata = await this._proxy.$prepareChatAccess(from, languageModelId, options.justification); + const metadata = this._allLanguageModelData.get(languageModelId)?.metadata; if (!metadata || !this._allLanguageModelData.has(languageModelId)) { throw extHostTypes.LanguageModelError.NotFound(`Language model '${languageModelId}' is unknown.`); @@ -324,20 +347,13 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { return res.apiObject; } - private _convertMessages(extension: IExtensionDescription, messages: (vscode.LanguageModelChatMessage2 | vscode.LanguageModelChatMessage)[]) { + private _convertMessages(extension: IExtensionDescription, messages: vscode.LanguageModelChatMessage[]) { const internalMessages: IChatMessage[] = []; for (const message of messages) { - if (message instanceof extHostTypes.LanguageModelChatMessage2) { - if (message.role as number === extHostTypes.LanguageModelChatMessageRole.System) { - checkProposedApiEnabled(extension, 'languageModelSystem'); - } - internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); - } else { - if (message instanceof extHostTypes.LanguageModelChatSystemMessage) { - checkProposedApiEnabled(extension, 'languageModelSystem'); - } - internalMessages.push(typeConvert.LanguageModelMessage.from(message)); + if (message.role as number === extHostTypes.LanguageModelChatMessageRole.System) { + checkProposedApiEnabled(extension, 'languageModelSystem'); } + internalMessages.push(typeConvert.LanguageModelChatMessage.from(message)); } return internalMessages; } @@ -366,8 +382,8 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { try { const detail = justification - ? localize('chatAccessWithJustification', "To allow access to the language models provided by {0}. Justification:\n\n{1}", to.displayName, justification) - : localize('chatAccess', "To allow access to the language models provided by {0}", to.displayName); + ? localize('chatAccessWithJustification', "Justification: {1}", to.displayName, justification) + : undefined; await this._extHostAuthentication.getSession(from, providerId, [], { forceNewSession: { detail } }); this.$updateModelAccesslist([{ from: from.identifier, to: to.identifier, enabled: true }]); return true; @@ -397,7 +413,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { } } - async computeTokenLength(languageModelId: string, value: string | vscode.LanguageModelChatMessage, token: vscode.CancellationToken): Promise { + private async _computeTokenLength(languageModelId: string, value: string | vscode.LanguageModelChatMessage, token: vscode.CancellationToken): Promise { const data = this._allLanguageModelData.get(languageModelId); if (!data) { @@ -410,21 +426,26 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { return local.provider.provideTokenCount(value, token); } - return this._proxy.$countTokens(data.identifier, (typeof value === 'string' ? value : typeConvert.LanguageModelMessage.from(value)), token); + return this._proxy.$countTokens(languageModelId, (typeof value === 'string' ? value : typeConvert.LanguageModelChatMessage.from(value)), token); } - getLanguageModelInfo(languageModelId: string): vscode.LanguageModelInformation | undefined { - const data = this._allLanguageModelData.get(languageModelId); - if (!data) { - return undefined; + $updateModelAccesslist(data: { from: ExtensionIdentifier; to: ExtensionIdentifier; enabled: boolean }[]): void { + const updated = new Array<{ from: ExtensionIdentifier; to: ExtensionIdentifier }>(); + for (const { from, to, enabled } of data) { + const set = this._modelAccessList.get(from) ?? new ExtensionIdentifierSet(); + const oldValue = set.has(to); + if (oldValue !== enabled) { + if (enabled) { + set.add(to); + } else { + set.delete(to); + } + this._modelAccessList.set(from, set); + const newItem = { from, to }; + updated.push(newItem); + this._onDidChangeModelAccess.fire(newItem); + } } - - return Object.freeze({ - id: data.identifier, - name: data.name, - version: data.version, - contextLength: data.tokens, - }); } private readonly _languageAccessInformationExtensions = new Set>(); @@ -441,13 +462,22 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { get onDidChange() { return Event.any(_onDidChangeAccess, _onDidAddRemove); }, - canSendRequest(languageModelId: string): boolean | undefined { + canSendRequest(chat: vscode.LanguageModelChat): boolean | undefined { - const data = that._allLanguageModelData.get(languageModelId); - if (!data) { + let metadata: ILanguageModelChatMetadata | undefined; + + out: for (const [_, value] of that._allLanguageModelData) { + for (const candidate of value.apiObjects.values()) { + if (candidate === chat) { + metadata = value.metadata; + break out; + } + } + } + if (!metadata) { return undefined; } - if (!that._isUsingAuth(from.identifier, data)) { + if (!that._isUsingAuth(from.identifier, metadata)) { return true; } @@ -455,7 +485,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { if (!list) { return undefined; } - return list.has(data.extension); + return list.has(metadata.extension); } }; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 75dc5a402ff..3a7e105c843 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -37,6 +37,7 @@ import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchMo 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 { ILogService } from 'vs/platform/log/common/log'; export class ExtHostNotebookController implements ExtHostNotebookShape { private static _notebookStatusBarItemProviderHandlePool: number = 0; @@ -78,7 +79,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { private _textDocumentsAndEditors: ExtHostDocumentsAndEditors, private _textDocuments: ExtHostDocuments, private _extHostFileSystem: IExtHostConsumerFileSystem, - private _extHostSearch: IExtHostSearch + private _extHostSearch: IExtHostSearch, + private _logService: ILogService ) { this._notebookProxy = mainContext.getProxy(MainContext.MainThreadNotebook); this._notebookDocumentsProxy = mainContext.getProxy(MainContext.MainThreadNotebookDocuments); @@ -314,6 +316,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { async $saveNotebook(handle: number, uriComponents: UriComponents, versionId: number, options: files.IWriteFileOptions, token: CancellationToken): Promise { const uri = URI.revive(uriComponents); const serializer = this._notebookSerializer.get(handle); + this.trace(`enter saveNotebook(versionId: ${versionId}, ${uri.toString()})`); + if (!serializer) { throw new Error('NO serializer found'); } @@ -331,14 +335,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { throw new files.FileOperationError(localize('err.readonly', "Unable to modify read-only file '{0}'", this._resourceForError(uri)), files.FileOperationResult.FILE_PERMISSION_DENIED); } - // validate write - await this._validateWriteFile(uri, options); - const data: vscode.NotebookData = { metadata: filter(document.apiNotebook.metadata, key => !(serializer.options?.transientDocumentMetadata ?? {})[key]), cells: [], }; + // this data must be retrieved before any async calls to ensure the data is for the correct version for (const cell of document.apiNotebook.getCells()) { const cellData = new extHostTypes.NotebookCellData( cell.kind, @@ -354,8 +356,21 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { data.cells.push(cellData); } + // validate write + await this._validateWriteFile(uri, options); + + if (token.isCancellationRequested) { + throw new Error('canceled'); + } const bytes = await serializer.serializer.serializeNotebook(data, token); + if (token.isCancellationRequested) { + throw new Error('canceled'); + } + + // Don't accept any cancellation beyond this point, we need to report the result of the file write + this.trace(`serialized versionId: ${versionId} ${uri.toString()}`); await this._extHostFileSystem.value.writeFile(uri, bytes); + this.trace(`Finished write versionId: ${versionId} ${uri.toString()}`); const providerExtUri = this._extHostFileSystem.getFileSystemProviderExtUri(uri.scheme); const stat = await this._extHostFileSystem.value.stat(uri); @@ -373,6 +388,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { children: undefined }; + this.trace(`exit saveNotebook(versionId: ${versionId}, ${uri.toString()})`); return fileStats; } @@ -716,4 +732,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { extHostCommands.registerApiCommand(commandDataToNotebook); extHostCommands.registerApiCommand(commandNotebookToData); } + + private trace(msg: string): void { + this._logService.trace(`[Extension Host Notebook] ${msg}`); + } } diff --git a/src/vs/workbench/api/common/extHostSpeech.ts b/src/vs/workbench/api/common/extHostSpeech.ts index abc56cedc08..198eaee26ad 100644 --- a/src/vs/workbench/api/common/extHostSpeech.ts +++ b/src/vs/workbench/api/common/extHostSpeech.ts @@ -36,7 +36,11 @@ export class ExtHostSpeech implements ExtHostSpeechShape { const cts = new CancellationTokenSource(); this.sessions.set(session, cts); - const speechToTextSession = disposables.add(provider.provideSpeechToTextSession(cts.token, language ? { language } : undefined)); + const speechToTextSession = await provider.provideSpeechToTextSession(cts.token, language ? { language } : undefined); + if (!speechToTextSession) { + return; + } + disposables.add(speechToTextSession.onDidChange(e => { if (cts.token.isCancellationRequested) { return; @@ -53,7 +57,7 @@ export class ExtHostSpeech implements ExtHostSpeechShape { this.sessions.delete(session); } - async $createTextToSpeechSession(handle: number, session: number): Promise { + async $createTextToSpeechSession(handle: number, session: number, language?: string): Promise { const provider = this.providers.get(handle); if (!provider) { return; @@ -64,7 +68,11 @@ export class ExtHostSpeech implements ExtHostSpeechShape { const cts = new CancellationTokenSource(); this.sessions.set(session, cts); - const textToSpeech = disposables.add(provider.provideTextToSpeechSession(cts.token)); + const textToSpeech = await provider.provideTextToSpeechSession(cts.token, language ? { language } : undefined); + if (!textToSpeech) { + return; + } + this.synthesizers.set(session, textToSpeech); disposables.add(textToSpeech.onDidChange(e => { @@ -79,12 +87,7 @@ export class ExtHostSpeech implements ExtHostSpeechShape { } async $synthesizeSpeech(session: number, text: string): Promise { - const synthesizer = this.synthesizers.get(session); - if (!synthesizer) { - return; - } - - synthesizer.synthesize(text); + this.synthesizers.get(session)?.synthesize(text); } async $cancelTextToSpeechSession(session: number): Promise { @@ -104,7 +107,11 @@ export class ExtHostSpeech implements ExtHostSpeechShape { const cts = new CancellationTokenSource(); this.sessions.set(session, cts); - const keywordRecognitionSession = disposables.add(provider.provideKeywordRecognitionSession(cts.token)); + const keywordRecognitionSession = await provider.provideKeywordRecognitionSession(cts.token); + if (!keywordRecognitionSession) { + return; + } + disposables.add(keywordRecognitionSession.onDidChange(e => { if (cts.token.isCancellationRequested) { return; diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index 896160e4d92..64a12869610 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -105,6 +105,7 @@ export class ExtHostTelemetry extends Disposable implements ExtHostTelemetryShap commonProperties['common.vscodemachineid'] = this.initData.telemetryInfo.machineId; commonProperties['common.vscodesessionid'] = this.initData.telemetryInfo.sessionId; commonProperties['common.sqmid'] = this.initData.telemetryInfo.sqmId; + commonProperties['common.devDeviceId'] = this.initData.telemetryInfo.devDeviceId; commonProperties['common.vscodeversion'] = this.initData.version; commonProperties['common.isnewappinstall'] = isNewAppInstall(this.initData.telemetryInfo.firstSessionDate); commonProperties['common.product'] = this.initData.environment.appHost; diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 27d76030c61..64458c0b585 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -23,11 +23,12 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocum import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostTestItemCollection, TestItemImpl, TestItemRootImpl, toItemFromContext } from 'vs/workbench/api/common/extHostTestItem'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestRunProfileKind, TestRunRequest } from 'vs/workbench/api/common/extHostTypes'; +import { TestRunProfileKind, TestRunRequest, FileCoverage } from 'vs/workbench/api/common/extHostTypes'; import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; -import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestErrorMessage, ITestItem, ITestItemContext, ITestMessageMenuArgs, ITestRunProfile, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestMessageFollowupRequest, TestMessageFollowupResponse, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; interface ControllerInfo { @@ -40,6 +41,10 @@ interface ControllerInfo { type DefaultProfileChangeEvent = Map>; +let followupCounter = 0; + +const testResultInternalIDs = new WeakMap(); + export class ExtHostTesting extends Disposable implements ExtHostTestingShape { private readonly resultsChangedEmitter = this._register(new Emitter()); protected readonly controllers = new Map(); @@ -47,14 +52,16 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { private readonly runTracker: TestRunCoordinator; private readonly observer: TestObservers; private readonly defaultProfilesChangedEmitter = this._register(new Emitter()); + private readonly followupProviders = new Set(); + private readonly testFollowups = new Map(); public onResultsChanged = this.resultsChangedEmitter.event; public results: ReadonlyArray = []; constructor( @IExtHostRpcService rpc: IExtHostRpcService, - @ILogService logService: ILogService, - commands: ExtHostCommands, + @ILogService private readonly logService: ILogService, + private readonly commands: ExtHostCommands, private readonly editors: ExtHostDocumentsAndEditors, ) { super(); @@ -154,7 +161,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return new TestItemImpl(controllerId, id, label, uri); }, createTestRun: (request, name, persist = true) => { - return this.runTracker.createTestRun(controllerId, collection, request, name, persist); + return this.runTracker.createTestRun(extension, controllerId, collection, request, name, persist); }, invalidateTestResults: items => { if (items === undefined) { @@ -210,7 +217,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { } await this.proxy.$runTests({ - preserveFocus: (req as vscode.TestRunRequest2).preserveFocus ?? true, + preserveFocus: req.preserveFocus ?? true, targets: [{ testIds: req.include?.map(t => TestId.fromExtHostTestItem(t, controller.collection.root.id).toString()) ?? [controller.collection.root.id], profileGroup: profileGroupToBitset[profile.kind], @@ -221,6 +228,14 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { }, token); } + /** + * Implements vscode.test.registerTestFollowupProvider + */ + public registerTestFollowupProvider(provider: vscode.TestFollowupProvider): vscode.Disposable { + this.followupProviders.add(provider); + return { dispose: () => { this.followupProviders.delete(provider); } }; + } + /** * @inheritdoc */ @@ -291,7 +306,11 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { public $publishTestResults(results: ISerializedTestResults[]): void { this.results = Object.freeze( results - .map(Convert.TestResults.to) + .map(r => { + const o = Convert.TestResults.to(r); + testResultInternalIDs.set(o, r.id); + return o; + }) .concat(this.results) .sort((a, b) => b.completedAt - a.completedAt) .slice(0, 32), @@ -347,13 +366,59 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { return res; } + /** @inheritdoc */ + public async $provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise { + const results = this.results.find(r => testResultInternalIDs.get(r) === req.resultId); + const test = results && findTestInResultSnapshot(TestId.fromString(req.extId), results?.results); + if (!test) { + return []; + } + + let followups: vscode.Command[] = []; + await Promise.all([...this.followupProviders].map(async provider => { + try { + const r = await provider.provideFollowup(results, test, req.taskIndex, req.messageIndex, token); + if (r) { + followups = followups.concat(r); + } + } catch (e) { + this.logService.error(`Error thrown while providing followup for test message`, e); + } + })); + + if (token.isCancellationRequested) { + return []; + } + + return followups.map(command => { + const id = followupCounter++; + this.testFollowups.set(id, command); + return { title: command.title, id }; + }); + } + + $disposeTestFollowups(id: number[]): void { + for (const i of id) { + this.testFollowups.delete(i); + } + } + + $executeTestFollowup(id: number): Promise { + const command = this.testFollowups.get(id); + if (!command) { + return Promise.resolve(); + } + + return this.commands.executeCommand(command.command, ...(command.arguments || [])); + } + private async runControllerTestRequest(req: ICallProfileRunHandler | ICallProfileRunHandler, isContinuous: boolean, token: CancellationToken): Promise { const lookup = this.controllers.get(req.controllerId); if (!lookup) { return {}; } - const { collection, profiles } = lookup; + const { collection, profiles, extension } = lookup; const profile = profiles.get(req.profileId); if (!profile) { return {}; @@ -382,6 +447,7 @@ export class ExtHostTesting extends Disposable implements ExtHostTestingShape { ); const tracker = isStartControllerTests(req) && this.runTracker.prepareForMainThreadTestRun( + extension, publicReq, TestRunDto.fromInternal(req, lookup.collection), profile, @@ -460,6 +526,7 @@ class TestRunTracker extends Disposable { private readonly proxy: MainThreadTestingShape, private readonly logService: ILogService, private readonly profile: vscode.TestRunProfile | undefined, + private readonly extension: IRelaxedExtensionDescription, parentToken?: CancellationToken, ) { super(); @@ -488,8 +555,8 @@ class TestRunTracker extends Disposable { /** Gets details for a previously-emitted coverage object. */ public getCoverageDetails(id: string, token: CancellationToken) { - const [, taskId, covId] = TestId.fromString(id).path; /** runId, taskId, URI */ - const coverage = this.publishedCoverage.get(covId); + const [, taskId] = TestId.fromString(id).path; /** runId, taskId, URI */ + const coverage = this.publishedCoverage.get(id); if (!coverage) { return []; } @@ -539,16 +606,44 @@ class TestRunTracker extends Disposable { }; let ended = false; + + // 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, name, onDidDispose: this.onDidDispose, - addCoverage: coverage => { + addCoverage: (coverage) => { + if (ended) { + return; + } + + const testItem = coverage instanceof FileCoverage ? coverage.testItem : undefined; + let testItemIdPart: undefined | number; + if (testItem) { + checkProposedApiEnabled(this.extension, 'attributableCoverage'); + if (!this.dto.isIncluded(testItem)) { + throw new Error('Attempted to `addCoverage` for a test item not included in the run'); + } + + this.ensureTestIsKnown(testItem); + testItemIdPart = testItemCoverageId.get(testItem); + if (testItemIdPart === undefined) { + testItemIdPart = testItemCoverageId.size; + testItemCoverageId.set(testItem, testItemIdPart); + } + } + const uriStr = coverage.uri.toString(); - const id = new TestId([runId, taskId, uriStr]).toString(); - this.publishedCoverage.set(uriStr, coverage); - this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(id, coverage)); + const id = new TestId(testItemIdPart !== undefined + ? [runId, taskId, uriStr, String(testItemIdPart)] + : [runId, taskId, uriStr], + ).toString(); + this.publishedCoverage.set(id, coverage); + this.proxy.$appendCoverage(runId, taskId, Convert.TestCoverage.fromFile(ctrlId, id, coverage)); }, //#region state mutation enqueued: guardTestMutation(test => { @@ -599,6 +694,7 @@ class TestRunTracker extends Disposable { } ended = true; + testItemCoverageId.clear(); this.proxy.$finishedTestRunTask(runId, taskId); if (!--this.running) { this.markEnded(); @@ -706,8 +802,8 @@ export class TestRunCoordinator { * `$startedExtensionTestRun` is not invoked. The run must eventually * be cancelled manually. */ - public prepareForMainThreadTestRun(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { - return this.getTracker(req, dto, profile, token); + public prepareForMainThreadTestRun(extension: IRelaxedExtensionDescription, req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile, token: CancellationToken) { + return this.getTracker(req, dto, profile, extension, token); } /** @@ -729,7 +825,7 @@ export class TestRunCoordinator { /** * Implements the public `createTestRun` API. */ - public createTestRun(controllerId: string, collection: ExtHostTestItemCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun { + public createTestRun(extension: IRelaxedExtensionDescription, 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); @@ -746,11 +842,11 @@ export class TestRunCoordinator { exclude: request.exclude?.map(t => TestId.fromExtHostTestItem(t, collection.root.id).toString()) ?? [], id: dto.id, include: request.include?.map(t => TestId.fromExtHostTestItem(t, collection.root.id).toString()) ?? [collection.root.id], - preserveFocus: (request as vscode.TestRunRequest2).preserveFocus ?? true, + preserveFocus: request.preserveFocus ?? true, persist }); - const tracker = this.getTracker(request, dto, request.profile); + const tracker = this.getTracker(request, dto, request.profile, extension); Event.once(tracker.onEnd)(() => { this.proxy.$finishedExtensionTestRun(dto.id); }); @@ -758,8 +854,8 @@ export class TestRunCoordinator { return tracker.createRun(name); } - private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, token?: CancellationToken) { - const tracker = new TestRunTracker(dto, this.proxy, this.logService, profile, token); + private getTracker(req: vscode.TestRunRequest, dto: TestRunDto, profile: vscode.TestRunProfile | undefined, extension: IRelaxedExtensionDescription, 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); return tracker; @@ -1170,3 +1266,20 @@ const profileGroupToBitset: { [K in TestRunProfileKind]: TestRunProfileBitset } [TestRunProfileKind.Debug]: TestRunProfileBitset.Debug, [TestRunProfileKind.Run]: TestRunProfileBitset.Run, }; + +function findTestInResultSnapshot(extId: TestId, snapshot: readonly Readonly[]) { + for (let i = 0; i < extId.path.length; i++) { + const item = snapshot.find(s => s.id === extId.path[i]); + if (!item) { + return undefined; + } + + if (i === extId.path.length - 1) { + return item; + } + + snapshot = item.children; + } + + return undefined; +} diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index f9571a57858..6525d0f2009 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -2052,7 +2052,7 @@ export namespace TestCoverage { } } - export function fromFile(id: string, coverage: vscode.FileCoverage): IFileCoverage.Serialized { + export function fromFile(controllerId: string, id: string, coverage: vscode.FileCoverage): IFileCoverage.Serialized { types.validateTestCoverageCount(coverage.statementCoverage); types.validateTestCoverageCount(coverage.branchCoverage); types.validateTestCoverageCount(coverage.declarationCoverage); @@ -2063,6 +2063,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, }; } } @@ -2242,45 +2244,19 @@ export namespace ChatFollowup { export namespace LanguageModelChatMessage { - export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage2 { - switch (message.role) { - case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatMessage2(types.LanguageModelChatMessageRole.System, message.content); - case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatMessage2(types.LanguageModelChatMessageRole.User, message.content); - case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatMessage2(types.LanguageModelChatMessageRole.Assistant, message.content); - } - } - - export function from(message: vscode.LanguageModelChatMessage2): 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 }; - } - } -} - -/** - * @deprecated - */ -export namespace LanguageModelMessage { - export function to(message: chatProvider.IChatMessage): vscode.LanguageModelChatMessage { switch (message.role) { - case chatProvider.ChatMessageRole.System: return new types.LanguageModelChatSystemMessage(message.content); - case chatProvider.ChatMessageRole.User: return new types.LanguageModelChatUserMessage(message.content); - case chatProvider.ChatMessageRole.Assistant: return new types.LanguageModelChatAssistantMessage(message.content); + 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); } } export function from(message: vscode.LanguageModelChatMessage): chatProvider.IChatMessage { - if (message instanceof types.LanguageModelChatSystemMessage) { - return { role: chatProvider.ChatMessageRole.System, content: message.content }; - } else if (message instanceof types.LanguageModelChatUserMessage) { - return { role: chatProvider.ChatMessageRole.User, content: message.content }; - } else if (message instanceof types.LanguageModelChatAssistantMessage) { - return { role: chatProvider.ChatMessageRole.Assistant, content: message.content }; - } else { - throw new Error('Invalid LanguageModelMessage'); + 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 }; } } } @@ -2323,6 +2299,17 @@ export namespace ChatResponseDetectedParticipantPart { } } +export namespace ChatResponseConfirmationPart { + export function from(part: vscode.ChatResponseConfirmationPart): Dto { + return { + kind: 'confirmation', + title: part.title, + message: part.message, + data: part.data + }; + } +} + export namespace ChatResponseFilesPart { export function from(part: vscode.ChatResponseFileTreePart): IChatTreeData { const { value, baseUri } = part; @@ -2364,10 +2351,13 @@ export namespace ChatResponseFilesPart { export namespace ChatResponseAnchorPart { export function from(part: vscode.ChatResponseAnchorPart): Dto { + // Work around type-narrowing confusion between vscode.Uri and URI + const isUri = (thing: unknown): thing is vscode.Uri => URI.isUri(thing); + return { kind: 'inlineReference', name: part.title, - inlineReference: !URI.isUri(part.value) ? Location.from(part.value) : part.value + inlineReference: isUri(part.value) ? part.value : Location.from(part.value) }; } @@ -2404,6 +2394,24 @@ export namespace ChatResponseWarningPart { } } +export namespace ChatTask { + export function from(part: vscode.ChatResponseProgressPart2): IChatTaskDto { + return { + kind: 'progressTask', + content: MarkdownString.from(part.value), + }; + } +} + +export namespace ChatTaskResult { + export function from(part: string | void): Dto { + return { + kind: 'progressTaskResult', + content: typeof part === 'string' ? MarkdownString.from(part) : undefined + }; + } +} + export namespace ChatResponseCommandButtonPart { export function from(part: vscode.ChatResponseCommandButtonPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): Dto { // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore @@ -2434,10 +2442,11 @@ export namespace ChatResponseTextEditPart { } export namespace ChatResponseReferencePart { - export function from(part: vscode.ChatResponseReferencePart): Dto { + export function from(part: types.ChatResponseReferencePart): Dto { const iconPath = ThemeIcon.isThemeIcon(part.iconPath) ? part.iconPath - : (part.iconPath && 'light' in part.iconPath && 'dark' in part.iconPath && URI.isUri(part.iconPath.light) && URI.isUri(part.iconPath.dark) ? { light: URI.revive(part.iconPath.light), dark: URI.revive(part.iconPath.dark) } - : undefined); + : URI.isUri(part.iconPath) ? { light: URI.revive(part.iconPath) } + : (part.iconPath && 'light' in part.iconPath && 'dark' in part.iconPath && URI.isUri(part.iconPath.light) && URI.isUri(part.iconPath.dark) ? { light: URI.revive(part.iconPath.light), dark: URI.revive(part.iconPath.dark) } + : undefined); if ('variableName' in part.value) { return { kind: 'reference', @@ -2472,13 +2481,13 @@ export namespace ChatResponseReferencePart { value: value.reference.value && mapValue(value.reference.value) } : mapValue(value.reference) - ); + ) as vscode.ChatResponseReferencePart; // 'value' is extended with variableName } } export namespace ChatResponsePart { - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseWarningPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { @@ -2540,8 +2549,10 @@ export namespace ChatAgentRequest { command: request.command, attempt: request.attempt ?? 0, enableCommandDetection: request.enableCommandDetection ?? true, - variables: request.variables.variables.map(ChatAgentValueReference.to), + references: request.variables.variables.map(ChatAgentValueReference.to), location: ChatLocation.to(request.location), + acceptedConfirmationData: request.acceptedConfirmationData, + rejectedConfirmationData: request.rejectedConfirmationData }; } } @@ -2555,19 +2566,32 @@ export namespace ChatLocation { case ChatAgentLocation.Editor: return types.ChatLocation.Editor; } } + + export function from(loc: types.ChatLocation): ChatAgentLocation { + switch (loc) { + case types.ChatLocation.Notebook: return ChatAgentLocation.Notebook; + case types.ChatLocation.Terminal: return ChatAgentLocation.Terminal; + case types.ChatLocation.Panel: return ChatAgentLocation.Panel; + case types.ChatLocation.Editor: return ChatAgentLocation.Editor; + } + } } export namespace ChatAgentValueReference { - export function to(request: IChatRequestVariableEntry): vscode.ChatValueReference { - const value = request.value; + export function to(variable: IChatRequestVariableEntry): vscode.ChatPromptReference { + const value = variable.value; if (!value) { throw new Error('Invalid value reference'); } return { - name: request.name, - range: (request.range && [request.range.start, request.range.endExclusive])!, // TODO - value: isUriComponents(value) ? URI.revive(value) : value, + id: variable.id, + name: variable.name, + range: variable.range && [variable.range.start, variable.range.endExclusive], + value: isUriComponents(value) ? URI.revive(value) : + value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri) ? + Location.to(revive(value)) : value, + modelDescription: variable.modelDescription }; } } @@ -2575,7 +2599,10 @@ export namespace ChatAgentValueReference { export namespace ChatAgentCompletionItem { export function from(item: vscode.ChatCompletionItem, commandsConverter: CommandsConverter, disposables: DisposableStore): extHostProtocol.IChatAgentCompletionItem { return { + id: item.id, label: item.label, + fullName: item.fullName, + icon: item.icon?.id, value: item.values[0].value, insertText: item.insertText, detail: item.detail, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index c52e1c26cc2..189c90b8f63 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4023,7 +4023,7 @@ export enum TestRunProfileKind { } @es5ClassCompat -export class TestRunRequest implements vscode.TestRunRequest2 { +export class TestRunRequest implements vscode.TestRunRequest { constructor( public readonly include: vscode.TestItem[] | undefined = undefined, public readonly exclude: vscode.TestItem[] | undefined = undefined, @@ -4119,6 +4119,7 @@ export class FileCoverage implements vscode.FileCoverage { public statementCoverage: vscode.TestCoverageCount, public branchCoverage?: vscode.TestCoverageCount, public declarationCoverage?: vscode.TestCoverageCount, + public testItem?: vscode.TestItem, ) { } } @@ -4269,14 +4270,18 @@ export enum ChatVariableLevel { } export class ChatCompletionItem implements vscode.ChatCompletionItem { + id: string; label: string | CompletionItemLabel; + fullName?: string | undefined; + icon?: vscode.ThemeIcon; insertText?: string; values: vscode.ChatVariableValue[]; detail?: string; documentation?: string | MarkdownString; command?: vscode.Command; - constructor(label: string | CompletionItemLabel, values: vscode.ChatVariableValue[]) { + constructor(id: string, label: string | CompletionItemLabel, values: vscode.ChatVariableValue[]) { + this.id = id; this.label = label; this.values = values; } @@ -4337,6 +4342,17 @@ export class ChatResponseDetectedParticipantPart { } } +export class ChatResponseConfirmationPart { + title: string; + message: string; + data: any; + constructor(title: string, message: string, data: any) { + this.title = title; + this.message = message; + this.data = data; + } +} + export class ChatResponseFileTreePart { value: vscode.ChatResponseFileTree[]; baseUri: vscode.Uri; @@ -4347,9 +4363,9 @@ export class ChatResponseFileTreePart { } export class ChatResponseAnchorPart { - value: vscode.Uri | vscode.Location | vscode.SymbolInformation; + value: vscode.Uri | vscode.Location; title?: string; - constructor(value: vscode.Uri | vscode.Location | vscode.SymbolInformation, title?: string) { + constructor(value: vscode.Uri | vscode.Location, title?: string) { this.value = value; this.title = title; } @@ -4362,6 +4378,15 @@ export class ChatResponseProgressPart { } } +export class ChatResponseProgressPart2 { + value: string; + task?: (progress: vscode.Progress) => Thenable; + constructor(value: string, task?: (progress: vscode.Progress) => Thenable) { + this.value = value; + this.task = task; + } +} + export class ChatResponseWarningPart { value: vscode.MarkdownString; constructor(value: string | vscode.MarkdownString) { @@ -4382,8 +4407,8 @@ export class ChatResponseCommandButtonPart { export class ChatResponseReferencePart { value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }; - iconPath?: vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }; - constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }) { + iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }; + constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }) { this.value = value; this.iconPath = iconPath; } @@ -4402,7 +4427,7 @@ export class ChatRequestTurn implements vscode.ChatRequestTurn { constructor( readonly prompt: string, readonly command: string | undefined, - readonly variables: vscode.ChatValueReference[], + readonly references: vscode.ChatPromptReference[], readonly participant: string, ) { } } @@ -4430,7 +4455,15 @@ export enum LanguageModelChatMessageRole { System = 3 } -export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessage2 { +export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage { + + static User(content: string, name?: string): LanguageModelChatMessage { + return new LanguageModelChatMessage(LanguageModelChatMessageRole.User, content, name); + } + + static Assistant(content: string, name?: string): LanguageModelChatMessage { + return new LanguageModelChatMessage(LanguageModelChatMessageRole.Assistant, content, name); + } role: vscode.LanguageModelChatMessageRole; content: string; @@ -4443,14 +4476,20 @@ export class LanguageModelChatMessage2 implements vscode.LanguageModelChatMessag } } +/** + * @deprecated + */ export class LanguageModelChatSystemMessage { content: string; - constructor(content: string) { this.content = content; } } + +/** + * @deprecated + */ export class LanguageModelChatUserMessage { content: string; name: string | undefined; @@ -4461,6 +4500,9 @@ export class LanguageModelChatUserMessage { } } +/** + * @deprecated + */ export class LanguageModelChatAssistantMessage { content: string; name?: string; diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 806a6b685dd..5a7ed7e4e70 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -66,7 +66,7 @@ suite('NotebookCell#Document', function () { override onExtensionError(): boolean { return true; } - }), extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch); + }), extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch, new NullLogService()); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); const reg = extHostNotebooks.registerNotebookSerializer(nullExtensionDescription, 'test', new class extends mock() { }); diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 8af3ece68d9..5a7e6f434c2 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -105,7 +105,7 @@ suite('NotebookKernel', function () { }); extHostConsumerFileSystem = new ExtHostConsumerFileSystem(rpcProtocol, new ExtHostFileSystemInfo()); extHostSearch = new ExtHostSearch(rpcProtocol, new URITransformerService(null), new NullLogService()); - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch); + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch, new NullLogService()); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 0f79b399d9d..97bfb308b9f 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -44,7 +44,8 @@ suite('ExtHostTelemetry', function () { firstSessionDate: '2020-01-01T00:00:00.000Z', sessionId: 'test', machineId: 'test', - sqmId: 'test' + sqmId: 'test', + devDeviceId: 'test' }; const mockRemote = { diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index adcd787f160..b82376cd88d 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -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 } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IRelaxedExtensionDescription } 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,6 +637,7 @@ suite('ExtHost Testing', () => { let req: TestRunRequest; let dto: TestRunDto; + const ext: IRelaxedExtensionDescription = {} as any; teardown(() => { for (const { id } of c.trackers) { @@ -658,6 +659,7 @@ suite('ExtHost Testing', () => { include: undefined, exclude: [single.root.children.get('id-b')!], profile: configuration, + preserveFocus: false, }; dto = TestRunDto.fromInternal({ @@ -670,11 +672,11 @@ suite('ExtHost Testing', () => { }); test('tracks a run started from a main thread request', () => { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); + const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token)); assert.strictEqual(tracker.hasRunningTasks, false); - const task1 = c.createTestRun('ctrl', single, req, 'run1', true); - const task2 = c.createTestRun('ctrl', single, req, 'run2', true); + const task1 = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); + const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true); assert.strictEqual(proxy.$startedExtensionTestRun.called, false); assert.strictEqual(tracker.hasRunningTasks, true); @@ -695,8 +697,8 @@ suite('ExtHost Testing', () => { test('run cancel force ends after a timeout', () => { const clock = sinon.useFakeTimers(); try { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); - const task = c.createTestRun('ctrl', single, req, 'run1', true); + const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token)); + const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); @@ -720,8 +722,8 @@ suite('ExtHost Testing', () => { }); test('run cancel force ends on second cancellation request', () => { - const tracker = ds.add(c.prepareForMainThreadTestRun(req, dto, configuration, cts.token)); - const task = c.createTestRun('ctrl', single, req, 'run1', true); + const tracker = ds.add(c.prepareForMainThreadTestRun(ext, req, dto, configuration, cts.token)); + const task = c.createTestRun(ext, 'ctrl', single, req, 'run1', true); const onEnded = sinon.stub(); ds.add(tracker.onEnd(onEnded)); @@ -739,7 +741,7 @@ suite('ExtHost Testing', () => { }); test('tracks a run started from an extension request', () => { - const task1 = c.createTestRun('ctrl', single, req, 'hello world', false); + const task1 = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; assert.strictEqual(tracker.hasRunningTasks, true); @@ -752,12 +754,12 @@ suite('ExtHost Testing', () => { exclude: [new TestId(['ctrlId', 'id-b']).toString()], persist: false, continuous: false, - preserveFocus: true, + preserveFocus: false, }] ]); - const task2 = c.createTestRun('ctrl', single, req, 'run2', true); - const task3Detached = c.createTestRun('ctrl', single, { ...req }, 'task3Detached', true); + const task2 = c.createTestRun(ext, 'ctrl', single, req, 'run2', true); + const task3Detached = c.createTestRun(ext, 'ctrl', single, { ...req }, 'task3Detached', true); task1.end(); assert.strictEqual(proxy.$finishedExtensionTestRun.called, false); @@ -771,7 +773,7 @@ suite('ExtHost Testing', () => { }); test('adds tests to run smartly', () => { - const task1 = c.createTestRun('ctrlId', single, req, 'hello world', false); + const task1 = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false); const tracker = Iterable.first(c.trackers)!; const expectedArgs: unknown[][] = []; assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs); @@ -810,7 +812,7 @@ suite('ExtHost Testing', () => { const test2 = new TestItemImpl('ctrlId', 'id-d', 'test d', URI.file('/testd.txt')); test1.range = test2.range = new Range(new Position(0, 0), new Position(1, 0)); single.root.children.replace([test1, test2]); - const task = c.createTestRun('ctrlId', single, req, 'hello world', false); + const task = c.createTestRun(ext, 'ctrlId', single, req, 'hello world', false); const message1 = new TestMessage('some message'); message1.location = new Location(URI.file('/a.txt'), new Position(0, 0)); @@ -851,7 +853,7 @@ suite('ExtHost Testing', () => { }); test('guards calls after runs are ended', () => { - const task = c.createTestRun('ctrl', single, req, 'hello world', false); + const task = c.createTestRun(ext, 'ctrl', single, req, 'hello world', false); task.end(); task.failed(single.root, new TestMessage('some message')); @@ -863,10 +865,11 @@ suite('ExtHost Testing', () => { }); test('excludes tests outside tree or explicitly excluded', () => { - const task = c.createTestRun('ctrlId', single, { + const task = c.createTestRun(ext, 'ctrlId', single, { profile: configuration, include: [single.root.children.get('id-a')!], exclude: [single.root.children.get('id-a')!.children.get('id-aa')!], + preserveFocus: false, }, 'hello world', false); task.passed(single.root.children.get('id-a')!.children.get('id-aa')!); @@ -892,7 +895,7 @@ suite('ExtHost Testing', () => { const childB = new TestItemImpl('ctrlId', 'id-child', 'child', undefined); testB!.children.replace([childB]); - const task1 = c.createTestRun('ctrl', single, new TestRunRequestImpl(), 'hello world', false); + const task1 = c.createTestRun(ext, 'ctrl', single, new TestRunRequestImpl(), 'hello world', false); const tracker = Iterable.first(c.trackers)!; task1.passed(childA); diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 12e356a7ea7..db6890b9017 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -18,11 +18,11 @@ import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { Table } from 'vs/base/browser/ui/table/tableWidget'; import { AbstractTree, TreeFindMatchType, TreeFindMode } from 'vs/base/browser/ui/tree/abstractTree'; -import { EventType, getActiveWindow, isActiveElement } from 'vs/base/browser/dom'; +import { isActiveElement } from 'vs/base/browser/dom'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { localize, localize2 } from 'vs/nls'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; function ensureDOMFocus(widget: ListWidget | undefined): void { // it can happen that one of the commands is executed while @@ -60,10 +60,6 @@ async function navigate(widget: WorkbenchListWidget | undefined, updateFocusFn: return; } - if (activeHover) { - toggleCustomHover(activeHover, widget); - } - await updateFocus(widget, updateFocusFn); const listFocus = widget.getFocus(); @@ -727,37 +723,27 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ return; } - // Check if the focused element has a hover, otherwise find the first child with a hover - const elementWithHover = focusedElement.matches('[custom-hover="true"]') ? focusedElement : focusedElement.querySelector('[custom-hover="true"]'); - if (!elementWithHover) { - return; + const elementWithHover = getCustomHoverForElement(focusedElement as HTMLElement); + if (elementWithHover) { + accessor.get(IHoverService).triggerUpdatableHover(elementWithHover as HTMLElement); } - - toggleCustomHover(elementWithHover as HTMLElement, lastFocusedList); }, }); -let activeHover: undefined | HTMLElement; -let disposable: IDisposable | undefined; -function toggleCustomHover(element: HTMLElement, list: WorkbenchListWidget) { - const show = !element.getAttribute('custom-hover-active'); - const mouseEvent = new MouseEvent(show ? EventType.MOUSE_OVER : EventType.MOUSE_LEAVE, { - view: getActiveWindow(), - bubbles: true, - cancelable: true, - }); - element.dispatchEvent(mouseEvent); - - if (activeHover === element && !show) { - activeHover = undefined; - disposable?.dispose(); - disposable = undefined; - } else { - activeHover = element; - disposable = list.onDidBlur(() => { - toggleCustomHover(element, list); - }); +function getCustomHoverForElement(element: HTMLElement): HTMLElement | undefined { + // Check if the element itself has a hover + if (element.matches('[custom-hover="true"]')) { + return element; } + + // Only consider children that are not action items or have a tabindex + // as these element are focusable and the user is able to trigger them already + const noneFocusableElementWithHover = element.querySelector('[custom-hover="true"]:not([tabindex]):not(.action-item)'); + if (noneFocusableElementWithHover) { + return noneFocusableElementWithHover as HTMLElement; + } + + return undefined; } KeybindingsRegistry.registerCommandAndKeybindingRule({ diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 71ec7e27232..c22caa967b3 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -7,44 +7,30 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from 'vs/platform/contextkey/common/contextkeys'; -import { SplitEditorsVertically, InEditorZenModeContext, ActiveEditorCanRevertContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorReadonlyContext, MainEditorAreaVisibleContext, ActiveEditorAvailableEditorIdsContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, ActiveEditorCanToggleReadonlyContext, applyAvailableEditorIds, TitleBarVisibleContext, TitleBarStyleContext, MultipleEditorGroupsContext, IsAuxiliaryWindowFocusedContext, ActiveCompareEditorCanSwapContext } from 'vs/workbench/common/contextkeys'; -import { TEXT_DIFF_EDITOR_ID, EditorInputCapabilities, SIDE_BY_SIDE_EDITOR_ID, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { SplitEditorsVertically, InEditorZenModeContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, TitleBarVisibleContext, TitleBarStyleContext, IsAuxiliaryWindowFocusedContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorGroupLockedContext, MultipleEditorGroupsContext, EditorsVisibleContext } from 'vs/workbench/common/contextkeys'; import { trackFocus, addDisposableListener, EventType, onDidRegisterWindow, getActiveWindow } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { WorkbenchState, IWorkspaceContextService, isTemporaryWorkspace } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchLayoutService, Parts, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { getVirtualWorkspaceScheme } from 'vs/platform/workspace/common/virtualWorkspace'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { isNative } from 'vs/base/common/platform'; -import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; import { IProductService } from 'vs/platform/product/common/productService'; -import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; import { getTitleBarStyle } from 'vs/platform/window/common/window'; import { mainWindow } from 'vs/base/browser/window'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { isFullscreen, onDidChangeFullscreen } from 'vs/base/browser/browser'; -import { Schemas } from 'vs/base/common/network'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; private dirtyWorkingCopiesContext: IContextKey; - private activeEditorContext: IContextKey; - private activeEditorCanRevert: IContextKey; - private activeEditorCanSplitInGroup: IContextKey; - private activeEditorAvailableEditorIds: IContextKey; - - private activeEditorIsReadonly: IContextKey; - private activeCompareEditorCanSwap: IContextKey; - private activeEditorCanToggleReadonly: IContextKey; - private activeEditorGroupEmpty: IContextKey; private activeEditorGroupIndex: IContextKey; private activeEditorGroupLast: IContextKey; @@ -53,10 +39,6 @@ export class WorkbenchContextKeysHandler extends Disposable { private editorsVisibleContext: IContextKey; - private textCompareEditorVisibleContext: IContextKey; - private textCompareEditorActiveContext: IContextKey; - - private sideBySideEditorActiveContext: IContextKey; private splitEditorsVerticallyContext: IContextKey; private workbenchStateContext: IContextKey; @@ -90,13 +72,11 @@ export class WorkbenchContextKeysHandler extends Disposable { @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IProductService private readonly productService: IProductService, - @IEditorService private readonly editorService: IEditorService, - @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, + @IEditorService private readonly editorService: IEditorService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, - @IFileService private readonly fileService: IFileService ) { super(); @@ -128,24 +108,16 @@ export class WorkbenchContextKeysHandler extends Disposable { ProductQualityContext.bindTo(this.contextKeyService).set(this.productService.quality || ''); EmbedderIdentifierContext.bindTo(this.contextKeyService).set(productService.embedderIdentifier); - // Editors - this.activeEditorContext = ActiveEditorContext.bindTo(this.contextKeyService); - this.activeEditorIsReadonly = ActiveEditorReadonlyContext.bindTo(this.contextKeyService); - this.activeCompareEditorCanSwap = ActiveCompareEditorCanSwapContext.bindTo(this.contextKeyService); - this.activeEditorCanToggleReadonly = ActiveEditorCanToggleReadonlyContext.bindTo(this.contextKeyService); - this.activeEditorCanRevert = ActiveEditorCanRevertContext.bindTo(this.contextKeyService); - this.activeEditorCanSplitInGroup = ActiveEditorCanSplitInGroupContext.bindTo(this.contextKeyService); - this.activeEditorAvailableEditorIds = ActiveEditorAvailableEditorIdsContext.bindTo(this.contextKeyService); - this.editorsVisibleContext = EditorsVisibleContext.bindTo(this.contextKeyService); - this.textCompareEditorVisibleContext = TextCompareEditorVisibleContext.bindTo(this.contextKeyService); - this.textCompareEditorActiveContext = TextCompareEditorActiveContext.bindTo(this.contextKeyService); - this.sideBySideEditorActiveContext = SideBySideEditorActiveContext.bindTo(this.contextKeyService); + // Editor Groups this.activeEditorGroupEmpty = ActiveEditorGroupEmptyContext.bindTo(this.contextKeyService); this.activeEditorGroupIndex = ActiveEditorGroupIndexContext.bindTo(this.contextKeyService); this.activeEditorGroupLast = ActiveEditorGroupLastContext.bindTo(this.contextKeyService); this.activeEditorGroupLocked = ActiveEditorGroupLockedContext.bindTo(this.contextKeyService); this.multipleEditorGroupsContext = MultipleEditorGroupsContext.bindTo(this.contextKeyService); + // Editors + this.editorsVisibleContext = EditorsVisibleContext.bindTo(this.contextKeyService); + // Working Copies this.dirtyWorkingCopiesContext = DirtyWorkingCopiesContext.bindTo(this.contextKeyService); this.dirtyWorkingCopiesContext.set(this.workingCopyService.hasDirty); @@ -231,18 +203,17 @@ export class WorkbenchContextKeysHandler extends Disposable { private registerListeners(): void { this.editorGroupService.whenReady.then(() => { this.updateEditorAreaContextKeys(); - this.updateEditorContextKeys(); + this.updateEditorGroupContextKeys(); + this.updateVisiblePanesContextKeys(); }); - this._register(this.editorService.onDidActiveEditorChange(() => this.updateEditorContextKeys())); - this._register(this.editorService.onDidVisibleEditorsChange(() => this.updateEditorContextKeys())); - - this._register(this.editorGroupService.onDidAddGroup(() => this.updateEditorContextKeys())); - this._register(this.editorGroupService.onDidRemoveGroup(() => this.updateEditorContextKeys())); - this._register(this.editorGroupService.onDidChangeGroupIndex(() => this.updateEditorContextKeys())); - - this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.updateEditorGroupContextKeys())); - this._register(this.editorGroupService.onDidChangeGroupLocked(() => this.updateEditorGroupContextKeys())); + this._register(this.editorService.onDidActiveEditorChange(() => this.updateEditorGroupContextKeys())); + 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.onDidChangeEditorPartOptions(() => this.updateEditorAreaContextKeys())); @@ -286,55 +257,25 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.dirtyWorkingCopiesContext.set(workingCopy.isDirty() || this.workingCopyService.hasDirty))); } - private updateEditorAreaContextKeys(): void { - this.editorTabsVisibleContext.set(this.editorGroupService.partOptions.showTabs === 'multiple'); - } - - private updateEditorContextKeys(): void { - const activeEditorPane = this.editorService.activeEditorPane; + private updateVisiblePanesContextKeys(): void { const visibleEditorPanes = this.editorService.visibleEditorPanes; - - this.textCompareEditorActiveContext.set(activeEditorPane?.getId() === TEXT_DIFF_EDITOR_ID); - this.textCompareEditorVisibleContext.set(visibleEditorPanes.some(editorPane => editorPane.getId() === TEXT_DIFF_EDITOR_ID)); - - this.sideBySideEditorActiveContext.set(activeEditorPane?.getId() === SIDE_BY_SIDE_EDITOR_ID); - if (visibleEditorPanes.length > 0) { this.editorsVisibleContext.set(true); } else { this.editorsVisibleContext.reset(); } + } + private updateEditorGroupContextKeys(): void { if (!this.editorService.activeEditor) { this.activeEditorGroupEmpty.set(true); } else { this.activeEditorGroupEmpty.reset(); } - - this.updateEditorGroupContextKeys(); - - if (activeEditorPane) { - this.activeEditorContext.set(activeEditorPane.getId()); - this.activeEditorCanRevert.set(!activeEditorPane.input.hasCapability(EditorInputCapabilities.Untitled)); - this.activeEditorCanSplitInGroup.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.CanSplitInGroup)); - applyAvailableEditorIds(this.activeEditorAvailableEditorIds, activeEditorPane.input, this.editorResolverService); - this.activeEditorIsReadonly.set(!!activeEditorPane.input.isReadonly()); - const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY }); - const secondaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.SECONDARY }); - this.activeCompareEditorCanSwap.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly() && !!primaryEditorResource && (this.fileService.hasProvider(primaryEditorResource) || primaryEditorResource.scheme === Schemas.untitled) && !!secondaryEditorResource && (this.fileService.hasProvider(secondaryEditorResource) || secondaryEditorResource.scheme === Schemas.untitled)); - this.activeEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly)); - } else { - this.activeEditorContext.reset(); - this.activeEditorIsReadonly.reset(); - this.activeCompareEditorCanSwap.reset(); - this.activeEditorCanToggleReadonly.reset(); - this.activeEditorCanRevert.reset(); - this.activeEditorCanSplitInGroup.reset(); - this.activeEditorAvailableEditorIds.reset(); - } + this.updateEditorGroupsContextKeys(); } - private updateEditorGroupContextKeys(): void { + private updateEditorGroupsContextKeys(): void { const groupCount = this.editorGroupService.count; if (groupCount > 1) { this.multipleEditorGroupsContext.set(true); @@ -348,6 +289,10 @@ export class WorkbenchContextKeysHandler extends Disposable { this.activeEditorGroupLocked.set(activeGroup.isLocked); } + private updateEditorAreaContextKeys(): void { + this.editorTabsVisibleContext.set(this.editorGroupService.partOptions.showTabs === 'multiple'); + } + private updateInputContextKeys(ownerDocument: Document): void { function activeElementIsInput(): boolean { diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 8d4313f5f15..35f856b931a 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -166,12 +166,15 @@ body.web { } .monaco-workbench .predefined-file-icon[class*='codicon-']::before { - font-family: 'codicon'; width: 16px; padding-left: 3px; /* width (16px) - font-size (13px) = padding-left (3px) */ padding-right: 3px; } +.predefined-file-icon::before { /* do add additional specificity to this selector, so it can be overridden by product themes */ + font-family: 'codicon'; +} + .monaco-workbench:not(.file-icons-enabled) .predefined-file-icon[class*='codicon-']::before { content: unset !important; } @@ -198,7 +201,6 @@ body.web { .monaco-workbench .select-container:after { content: var(--vscode-icon-chevron-down-content); font-family: var(--vscode-icon-chevron-down-font-family); - font-family: codicon; font-size: 16px; width: 16px; height: 16px; diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 3ea632e64d5..c86d7fd78f5 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -187,9 +187,9 @@ export abstract class CompositePart extends Part { this._register(that.onDidCompositeClose.event(e => this.onScopeClosed(e.getId()))); } }()); - const compositeInstantiationService = this.instantiationService.createChild(new ServiceCollection( + const compositeInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IEditorProgressService, compositeProgressIndicator] // provide the editor progress service for any editors instantiated within the composite - )); + ))); const composite = compositeDescriptor.instantiate(compositeInstantiationService); const disposable = new DisposableStore(); @@ -199,6 +199,7 @@ export abstract class CompositePart extends Part { // Register to title area update events from the composite disposable.add(composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId()), this)); + disposable.add(compositeInstantiationService); return composite; } diff --git a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts index f0341568cf0..279fc5713c4 100644 --- a/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts +++ b/src/vs/workbench/browser/parts/editor/auxiliaryEditorPart.ts @@ -203,10 +203,10 @@ export class AuxiliaryEditorPart { auxiliaryWindow.layout(); // Have a InstantiationService that is scoped to the auxiliary window - const instantiationService = this.instantiationService.createChild(new ServiceCollection( + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection( [IStatusbarService, this.statusbarService.createScoped(statusbarPart, disposables)], [IEditorService, this.editorService.createScoped(editorPart, disposables)] - )); + ))); return { part: editorPart, diff --git a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts index 1a84d021384..378552f35e0 100644 --- a/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/diffEditorCommands.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { localize, localize2 } from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; @@ -31,7 +33,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: TextCompareEditorVisibleContext, primary: KeyMod.Alt | KeyCode.F5, - handler: accessor => navigateInDiffEditor(accessor, true) + handler: (accessor, ...args) => navigateInDiffEditor(accessor, args, true) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { @@ -46,7 +48,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: TextCompareEditorVisibleContext, primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F5, - handler: accessor => navigateInDiffEditor(accessor, false) + handler: (accessor, ...args) => navigateInDiffEditor(accessor, args, false) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { @@ -56,11 +58,12 @@ export function registerDiffEditorCommands(): void { } }); - function getActiveTextDiffEditor(accessor: ServicesAccessor): TextDiffEditor | undefined { + function getActiveTextDiffEditor(accessor: ServicesAccessor, args: any[]): TextDiffEditor | undefined { const editorService = accessor.get(IEditorService); + const resource = args.length > 0 && args[0] instanceof URI ? args[0] : undefined; for (const editor of [editorService.activeEditorPane, ...editorService.visibleEditorPanes]) { - if (editor instanceof TextDiffEditor) { + if (editor instanceof TextDiffEditor && (!resource || editor.input instanceof DiffEditorInput && isEqual(editor.input.primary.resource, resource))) { return editor; } } @@ -68,8 +71,8 @@ export function registerDiffEditorCommands(): void { return undefined; } - function navigateInDiffEditor(accessor: ServicesAccessor, next: boolean): void { - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + function navigateInDiffEditor(accessor: ServicesAccessor, args: any[], next: boolean): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); if (activeTextDiffEditor) { activeTextDiffEditor.getControl()?.goToDiff(next ? 'next' : 'previous'); @@ -82,8 +85,8 @@ export function registerDiffEditorCommands(): void { Toggle } - function focusInDiffEditor(accessor: ServicesAccessor, mode: FocusTextDiffEditorMode): void { - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + function focusInDiffEditor(accessor: ServicesAccessor, args: any[], mode: FocusTextDiffEditorMode): void { + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); if (activeTextDiffEditor) { switch (mode) { @@ -95,17 +98,17 @@ export function registerDiffEditorCommands(): void { break; case FocusTextDiffEditorMode.Toggle: if (activeTextDiffEditor.getControl()?.getModifiedEditor().hasWidgetFocus()) { - return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original); + return focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Original); } else { - return focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified); + return focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Modified); } } } } - function toggleDiffSideBySide(accessor: ServicesAccessor): void { + function toggleDiffSideBySide(accessor: ServicesAccessor, args: any[]): void { const configService = accessor.get(ITextResourceConfigurationService); - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); const m = activeTextDiffEditor?.getControl()?.getModifiedEditor()?.getModel(); if (!m) { return; } @@ -115,9 +118,9 @@ export function registerDiffEditorCommands(): void { configService.updateValue(m.uri, key, !val); } - function toggleDiffIgnoreTrimWhitespace(accessor: ServicesAccessor): void { + function toggleDiffIgnoreTrimWhitespace(accessor: ServicesAccessor, args: any[]): void { const configService = accessor.get(ITextResourceConfigurationService); - const activeTextDiffEditor = getActiveTextDiffEditor(accessor); + const activeTextDiffEditor = getActiveTextDiffEditor(accessor, args); const m = activeTextDiffEditor?.getControl()?.getModifiedEditor()?.getModel(); if (!m) { return; } @@ -127,10 +130,10 @@ export function registerDiffEditorCommands(): void { configService.updateValue(m.uri, key, !val); } - async function swapDiffSides(accessor: ServicesAccessor): Promise { + async function swapDiffSides(accessor: ServicesAccessor, args: any[]): Promise { const editorService = accessor.get(IEditorService); - const diffEditor = getActiveTextDiffEditor(accessor); + const diffEditor = getActiveTextDiffEditor(accessor, args); const activeGroup = diffEditor?.group; const diffInput = diffEditor?.input; if (!diffEditor || typeof activeGroup === 'undefined' || !(diffInput instanceof DiffEditorInput) || !diffInput.modified.resource) { @@ -179,7 +182,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => toggleDiffSideBySide(accessor) + handler: (accessor, ...args) => toggleDiffSideBySide(accessor, args) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -187,7 +190,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Modified) + handler: (accessor, ...args) => focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Modified) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -195,7 +198,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Original) + handler: (accessor, ...args) => focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Original) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -203,7 +206,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => focusInDiffEditor(accessor, FocusTextDiffEditorMode.Toggle) + handler: (accessor, ...args) => focusInDiffEditor(accessor, args, FocusTextDiffEditorMode.Toggle) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -211,7 +214,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => toggleDiffIgnoreTrimWhitespace(accessor) + handler: (accessor, ...args) => toggleDiffIgnoreTrimWhitespace(accessor, args) }); KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -219,7 +222,7 @@ export function registerDiffEditorCommands(): void { weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: accessor => swapDiffSides(accessor) + handler: (accessor, ...args) => swapDiffSides(accessor, args) }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 1611076ea51..dfb20dc68fb 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -11,7 +11,7 @@ import { TextCompareEditorActiveContext, ActiveEditorPinnedContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorAvailableEditorIdsContext, EditorPartMultipleEditorGroupsContext, ActiveEditorDirtyContext, ActiveEditorGroupLockedContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext, EditorTabsVisibleContext, ActiveEditorLastInGroupContext, EditorPartMaximizedEditorGroupContext, MultipleEditorGroupsContext, InEditorZenModeContext, - IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext + IsAuxiliaryEditorPartContext, ActiveCompareEditorCanSwapContext, MultipleEditorsSelectedInGroupContext } from 'vs/workbench/common/contextkeys'; import { SideBySideEditorInput, SideBySideEditorInputSerializer } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; @@ -388,7 +388,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorActionsPositionSubmenu, { command: { id // Editor Title Context Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITOR_COMMAND_ID, title: localize('close', "Close") }, group: '1_close', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeOthers', "Close Others"), precondition: EditorGroupEditorsCountContext.notEqualsTo('1') }, group: '1_close', order: 20 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: localize('closeRight', "Close to the Right"), precondition: ActiveEditorLastInGroupContext.toNegated() }, group: '1_close', order: 30, when: EditorTabsVisibleContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_TO_THE_RIGHT_COMMAND_ID, title: localize('closeRight', "Close to the Right"), precondition: ContextKeyExpr.and(ActiveEditorLastInGroupContext.toNegated(), MultipleEditorsSelectedInGroupContext.negate()) }, group: '1_close', order: 30, when: EditorTabsVisibleContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_SAVED_EDITORS_COMMAND_ID, title: localize('closeAllSaved', "Close Saved") }, group: '1_close', order: 40 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeAll', "Close All") }, group: '1_close', order: 50 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: REOPEN_WITH_COMMAND_ID, title: localize('reopenWith', "Reopen Editor With...") }, group: '1_open', order: 10, when: ActiveEditorAvailableEditorIdsContext }); @@ -399,10 +399,11 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_ED MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_DOWN, title: localize('splitDown', "Split Down") }, group: '5_split', order: 20 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_LEFT, title: localize('splitLeft', "Split Left") }, group: '5_split', order: 30 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_RIGHT, title: localize('splitRight', "Split Right") }, group: '5_split', order: 40 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_IN_GROUP, title: localize('splitInGroup', "Split in Group") }, group: '6_split_in_group', order: 10, when: ActiveEditorCanSplitInGroupContext }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: JOIN_EDITOR_IN_GROUP, title: localize('joinInGroup', "Join in Group") }, group: '6_split_in_group', order: 10, when: SideBySideEditorActiveContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: SPLIT_EDITOR_IN_GROUP, title: localize('splitInGroup', "Split in Group"), precondition: MultipleEditorsSelectedInGroupContext.negate() }, group: '6_split_in_group', order: 10, when: ActiveEditorCanSplitInGroupContext }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: JOIN_EDITOR_IN_GROUP, title: localize('joinInGroup', "Join in Group"), precondition: MultipleEditorsSelectedInGroupContext.negate() }, group: '6_split_in_group', order: 10, when: SideBySideEditorActiveContext }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, title: localize('moveToNewWindow', "Move into New Window") }, group: '7_new_window', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, title: localize('copyToNewWindow', "Copy into New Window") }, group: '7_new_window', order: 20 }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { submenu: MenuId.EditorTitleContextShare, title: localize('share', "Share"), group: '11_share', order: -1, when: MultipleEditorsSelectedInGroupContext.negate() }); // Editor Title Menu MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_DIFF_SIDE_BY_SIDE, title: localize('inlineView', "Inline View"), toggled: ContextKeyExpr.equals('config.diffEditor.renderSideBySide', false) }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') }); diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index ead5ace8b41..138541a6ecf 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -18,6 +18,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IWindowsConfiguration } from 'vs/platform/window/common/window'; import { BooleanVerifier, EnumVerifier, NumberVerifier, ObjectVerifier, SetVerifier, verifyObject } from 'vs/base/common/verifier'; import { IAuxiliaryWindowOpenOptions } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { ContextKeyValue, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export interface IEditorPartCreationOptions { readonly restorePreviousState: boolean; @@ -32,6 +33,7 @@ export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { tabActionLocation: 'right', tabActionCloseVisibility: true, tabActionUnpinVisibility: true, + alwaysShowEditorActions: false, tabSizing: 'fit', tabSizingFixedMinWidth: 50, tabSizingFixedMaxWidth: 160, @@ -121,6 +123,7 @@ function validateEditorPartOptions(options: IEditorPartOptions): IEditorPartOpti 'highlightModifiedTabs': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['highlightModifiedTabs']), 'tabActionCloseVisibility': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['tabActionCloseVisibility']), 'tabActionUnpinVisibility': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['tabActionUnpinVisibility']), + 'alwaysShowEditorActions': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['alwaysShowEditorActions']), 'pinnedTabsOnSeparateRow': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['pinnedTabsOnSeparateRow']), 'focusRecentEditorAfterClose': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['focusRecentEditorAfterClose']), 'showIcons': new BooleanVerifier(DEFAULT_EDITOR_PART_OPTIONS['showIcons']), @@ -185,6 +188,8 @@ export interface IEditorPartsView { readonly count: number; createAuxiliaryEditorPart(options?: IAuxiliaryWindowOpenOptions): Promise; + + bind(contextKey: RawContextKey, group: IEditorGroupView): IContextKey; } /** @@ -333,6 +338,11 @@ export interface IInternalEditorOpenOptions extends IInternalEditorTitleControlO * the top that the editor opens in. */ readonly preserveWindowOrder?: boolean; + + /** + * Inactive editors to select after opening the active selected editor. + */ + readonly inactiveSelection?: EditorInput[]; } export interface IInternalEditorCloseOptions extends IInternalEditorTitleControlOptions { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 921a28459cc..b4665e6d8b2 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 } 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, 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 { 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'; @@ -61,11 +61,12 @@ abstract class AbstractSplitEditorAction extends Action2 { return preferredSideBySideGroupDirection(configurationService); } - override async run(accessor: ServicesAccessor, context?: IEditorIdentifier): Promise { + override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { const editorGroupService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); - splitEditor(editorGroupService, this.getDirection(configurationService), context); + const commandContext = getCommandsContext(accessor, resourceOrContext, context); + splitEditor(editorGroupService, this.getDirection(configurationService), commandContext ? [commandContext] : undefined); } } @@ -1146,7 +1147,7 @@ export class ToggleMaximizeEditorGroupAction extends Action2 { override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { const editorGroupsService = accessor.get(IEditorGroupsService); - const { group } = resolveCommandsContext(editorGroupsService, getCommandsContext(resourceOrContext, context)); + const { group } = resolveCommandsContext(editorGroupsService, getCommandsContext(accessor, resourceOrContext, context)); editorGroupsService.toggleMaximizeGroup(group); } } @@ -2514,19 +2515,22 @@ abstract class BaseMoveCopyEditorToNewWindowAction extends Action2 { override async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) { const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - const auxiliaryEditorPart = await editorGroupService.createAuxiliaryEditorPart(); - - if (this.move) { - group.moveEditor(editor, auxiliaryEditorPart.activeGroup); - } else { - group.copyEditor(editor, auxiliaryEditorPart.activeGroup); - } - - auxiliaryEditorPart.activeGroup.focus(); + const editorsContext = resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context)); + if (editorsContext.length === 0) { + 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); + if (this.move) { + sourceGroup.moveEditors(sourceEditors, auxiliaryEditorPart.activeGroup); + } else { + sourceGroup.copyEditors(sourceEditors, 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 f704886061d..c8670de6fb0 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -9,7 +9,7 @@ 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 } from 'vs/base/common/resources'; +import { extname, isEqual } 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'; @@ -36,7 +36,7 @@ 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, isEditorGroup, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService, IEditorReplacement, isEditorGroup, 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'; @@ -656,36 +656,50 @@ function registerFocusEditorGroupAtIndexCommands(): void { } } -export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, context?: IEditorCommandsContext): void { +export function splitEditor(editorGroupService: IEditorGroupsService, direction: GroupDirection, contexts?: IEditorCommandsContext[]): void { + let newGroup: IEditorGroup | undefined; let sourceGroup: IEditorGroup | undefined; - if (context && typeof context.groupId === 'number') { - sourceGroup = editorGroupService.getGroup(context.groupId); - } else { - sourceGroup = editorGroupService.activeGroup; - } - if (!sourceGroup) { - return; - } + for (const context of contexts ?? [undefined]) { + let currentGroup: IEditorGroup | undefined; - // Add group - const newGroup = editorGroupService.addGroup(sourceGroup, direction); + if (context) { + currentGroup = editorGroupService.getGroup(context.groupId); + } else { + currentGroup = editorGroupService.activeGroup; + } - // Split editor (if it can be split) - let editorToCopy: EditorInput | undefined; - if (context && typeof context.editorIndex === 'number') { - editorToCopy = sourceGroup.getEditorByIndex(context.editorIndex); - } else { - editorToCopy = sourceGroup.activeEditor ?? undefined; - } + if (!currentGroup) { + continue; + } - // Copy the editor to the new group, else create an empty group - if (editorToCopy && !editorToCopy.hasCapability(EditorInputCapabilities.Singleton)) { - sourceGroup.copyEditor(editorToCopy, newGroup, { preserveFocus: context?.preserveFocus }); + if (!sourceGroup) { + sourceGroup = currentGroup; + } else if (sourceGroup.id !== currentGroup.id) { + continue; // Only support splitting from the same group + } + + // Add group + if (!newGroup) { + newGroup = editorGroupService.addGroup(currentGroup, direction); + } + + // 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 }); + } } // Focus - newGroup.focus(); + newGroup?.focus(); } function registerSplitEditorCommands() { @@ -696,7 +710,8 @@ function registerSplitEditorCommands() { { id: SPLIT_EDITOR_RIGHT, direction: GroupDirection.RIGHT } ].forEach(({ id, direction }) => { CommandsRegistry.registerCommand(id, function (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) { - splitEditor(accessor.get(IEditorGroupsService), direction, getCommandsContext(resourceOrContext, context)); + const { editors } = getEditorsContext(accessor, resourceOrContext, context); + splitEditor(accessor.get(IEditorGroupsService), direction, editors); }); }); } @@ -793,7 +808,7 @@ function registerCloseEditorCommands() { win: { primary: KeyMod.CtrlCmd | KeyCode.F4, secondary: [KeyMod.CtrlCmd | KeyCode.KeyW] }, handler: (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); - const commandsContext = getCommandsContext(resourceOrContext, context); + const commandsContext = getCommandsContext(accessor, resourceOrContext, context); let group: IEditorGroup | undefined; if (commandsContext && typeof commandsContext.groupId === 'number') { @@ -858,7 +873,7 @@ function registerCloseEditorCommands() { handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (group && editor) { if (group.activeEditor) { group.pinEditor(group.activeEditor); @@ -875,70 +890,75 @@ function registerCloseEditorCommands() { when: undefined, primary: undefined, handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); const editorService = accessor.get(IEditorService); const editorResolverService = accessor.get(IEditorResolverService); const telemetryService = accessor.get(ITelemetryService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const editorsAndGroup = resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context)); + const editorReplacements = new Map(); - if (!editor) { - return; - } - const untypedEditor = editor.toUntyped(); + for (const { editor, group } of editorsAndGroup) { + const untypedEditor = editor.toUntyped(); + if (!untypedEditor) { + return; // Resolver can only resolve untyped editors + } - // Resolver can only resolve untyped editors - if (!untypedEditor) { - return; - } - 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; + } - // Replace editor with resolved one - await resolvedEditor.group.replaceEditors([ - { + 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 - } - ]); + }); - 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' }; - }; + // 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; - }; + 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 ?? '' - }); + telemetryService.publicLog2('workbenchEditorReopen', { + scheme: editor.resource?.scheme ?? '', + ext: editor.resource ? extname(editor.resource) : '', + from: editor.editorId ?? '', + to: resolvedEditor.editor.editorId ?? '' + }); + } - // Make sure it becomes active too - await resolvedEditor.group.openEditor(resolvedEditor.editor); + // Replace editor with resolved one and make active + for (const [group, replacements] of editorReplacements) { + await group.replaceEditors(replacements); + await group.openEditor(replacements[0].replacement); + } } }); CommandsRegistry.registerCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, async (accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); - const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (group) { await group.closeAllEditors(); @@ -986,7 +1006,7 @@ function registerSplitEditorInGroupCommands(): void { const editorGroupService = accessor.get(IEditorGroupsService); const instantiationService = accessor.get(IInstantiationService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (!editor) { return; } @@ -1021,7 +1041,7 @@ function registerSplitEditorInGroupCommands(): void { async function joinEditorInGroup(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { const editorGroupService = accessor.get(IEditorGroupsService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (!(editor instanceof SideBySideEditorInput)) { return; } @@ -1077,7 +1097,7 @@ function registerSplitEditorInGroupCommands(): void { async run(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): Promise { const editorGroupService = accessor.get(IEditorGroupsService); - const { editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (editor instanceof SideBySideEditorInput) { await joinEditorInGroup(accessor, resourceOrContext, context); } else if (editor) { @@ -1198,7 +1218,7 @@ function registerOtherEditorCommands(): void { handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { const editorGroupService = accessor.get(IEditorGroupsService); - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); if (group && editor) { return group.pinEditor(editor); } @@ -1219,7 +1239,7 @@ 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(resourceOrContext, context)); + const { group } = resolveCommandsContext(editorGroupService, getCommandsContext(accessor, resourceOrContext, context)); group?.lock(locked ?? !group.isLocked); } @@ -1273,11 +1293,8 @@ function registerOtherEditorCommands(): void { when: ActiveEditorStickyContext.toNegated(), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - return group.stickEditor(editor); + for (const { editor, group } of resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context))) { + group.stickEditor(editor); } } }); @@ -1315,11 +1332,8 @@ function registerOtherEditorCommands(): void { when: ActiveEditorStickyContext, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.Shift | KeyCode.Enter), handler: async (accessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { - const editorGroupService = accessor.get(IEditorGroupsService); - - const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); - if (group && editor) { - return group.unstickEditor(editor); + for (const { editor, group } of resolveEditorsContext(getEditorsContext(accessor, resourceOrContext, context))) { + group.unstickEditor(editor); } } }); @@ -1333,7 +1347,7 @@ function registerOtherEditorCommands(): void { const editorGroupService = accessor.get(IEditorGroupsService); const quickInputService = accessor.get(IQuickInputService); - const commandsContext = getCommandsContext(resourceOrContext, context); + const commandsContext = getCommandsContext(accessor, resourceOrContext, context); if (commandsContext && typeof commandsContext.groupId === 'number') { const group = editorGroupService.getGroup(commandsContext.groupId); if (group) { @@ -1346,11 +1360,12 @@ function registerOtherEditorCommands(): void { }); } -function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): { editors: IEditorCommandsContext[]; groups: Array } { +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(resourceOrContext, context), listService, editorGroupService); + const editorContext = getMultiSelectedEditorContexts(getCommandsContext(accessor, resourceOrContext, context), listService, editorGroupService); const activeGroup = editorGroupService.activeGroup; if (editorContext.length === 0 && activeGroup.activeEditor) { @@ -1367,17 +1382,38 @@ function getEditorsContext(accessor: ServicesAccessor, resourceOrContext?: URI | }; } -export function getCommandsContext(resourceOrContext?: URI | IEditorCommandsContext, context?: IEditorCommandsContext): IEditorCommandsContext | undefined { - if (URI.isUri(resourceOrContext)) { - return context; +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 (resourceOrContext && typeof resourceOrContext.groupId === 'number') { - return resourceOrContext; - } - - if (context && typeof context.groupId === 'number') { - return context; + 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; @@ -1432,6 +1468,17 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsCon 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] : []; diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 14f3ad5b728..463b527e3b6 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -165,7 +165,7 @@ class DropOverlay extends Themable { isCopy = this.isCopyOperation(e); } else if (isDraggingEditor) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { isCopy = this.isCopyOperation(e, data[0].identifier); } } @@ -234,7 +234,7 @@ class DropOverlay extends Themable { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { return this.editorGroupService.getGroup(data[0].identifier); } } @@ -242,7 +242,7 @@ class DropOverlay extends Themable { // Check for editor transfer else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { return this.editorGroupService.getGroup(data[0].identifier.groupId); } } @@ -267,7 +267,7 @@ class DropOverlay extends Themable { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const sourceGroup = this.editorGroupService.getGroup(data[0].identifier); if (sourceGroup) { if (typeof splitDirection !== 'number' && sourceGroup === this.groupView) { @@ -306,12 +306,13 @@ class DropOverlay extends Themable { // Check for editor transfer else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { - const draggedEditor = data[0].identifier; + if (Array.isArray(data) && data.length > 0) { + const draggedEditors = data; + const firstDraggedEditor = data[0].identifier; - const sourceGroup = this.editorGroupService.getGroup(draggedEditor.groupId); + const sourceGroup = this.editorGroupService.getGroup(firstDraggedEditor.groupId); if (sourceGroup) { - const copyEditor = this.isCopyOperation(event, draggedEditor); + const copyEditor = this.isCopyOperation(event, firstDraggedEditor); let targetGroup: IEditorGroup | undefined = undefined; // Optimization: if we move the last editor of an editor group @@ -328,16 +329,20 @@ class DropOverlay extends Themable { return; } - // Open in target group - const options = fillActiveEditorViewState(sourceGroup, draggedEditor.editor, { - pinned: true, // always pin dropped editor - sticky: sourceGroup.isSticky(draggedEditor.editor), // preserve sticky state - }); + const editors = draggedEditors.map(draggedEditor => ( + { + editor: draggedEditor.identifier.editor, + options: fillActiveEditorViewState(sourceGroup, draggedEditor.identifier.editor, { + pinned: true, // always pin dropped editor + sticky: sourceGroup.isSticky(draggedEditor.identifier.editor) // preserve sticky state + }) + } + )); if (!copyEditor) { - sourceGroup.moveEditor(draggedEditor.editor, targetGroup, options); + sourceGroup.moveEditors(editors, targetGroup); } else { - sourceGroup.copyEditor(draggedEditor.editor, targetGroup, options); + sourceGroup.copyEditors(editors, targetGroup); } } @@ -352,7 +357,7 @@ class DropOverlay extends Themable { // Check for tree items else if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) { const data = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const editors: IUntypedEditorInput[] = []; for (const id of data) { const dataTransferItem = await this.treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index dbe3d63a86e..d88cd673186 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -5,8 +5,8 @@ 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 } from 'vs/workbench/common/editor'; -import { ActiveEditorGroupLockedContext, ActiveEditorDirtyContext, EditorGroupEditorsCountContext, ActiveEditorStickyContext, ActiveEditorPinnedContext, ActiveEditorLastInGroupContext, ActiveEditorFirstInGroupContext, ResourceContextKey, applyAvailableEditorIds, ActiveEditorAvailableEditorIdsContext, ActiveEditorCanSplitInGroupContext, SideBySideEditorActiveContext } from 'vs/workbench/common/contextkeys'; +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 { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; @@ -56,6 +56,8 @@ import { EditorTitleControl } from 'vs/workbench/browser/parts/editor/editorTitl import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -157,7 +159,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { @ILogService private readonly logService: ILogService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @IHostService private readonly hostService: IHostService, - @IDialogService private readonly dialogService: IDialogService + @IDialogService private readonly dialogService: IDialogService, + @IFileService private readonly fileService: IFileService ) { super(themeService); @@ -194,10 +197,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.progressBar.hide(); // Scoped instantiation service - this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( + this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, this.scopedContextKeyService], [IEditorProgressService, this._register(new EditorProgressIndicator(this.progressBar, this))] - )); + ))); // Context keys this.resourceContext = this._register(this.scopedInstantiationService.createInstance(ResourceContextKey)); @@ -245,17 +248,28 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private handleGroupContextKeys(): void { - const groupActiveEditorDirtyContext = ActiveEditorDirtyContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorPinnedContext = ActiveEditorPinnedContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorFirstContext = ActiveEditorFirstInGroupContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorLastContext = ActiveEditorLastInGroupContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorStickyContext = ActiveEditorStickyContext.bindTo(this.scopedContextKeyService); - const groupEditorsCountContext = EditorGroupEditorsCountContext.bindTo(this.scopedContextKeyService); - const groupLockedContext = ActiveEditorGroupLockedContext.bindTo(this.scopedContextKeyService); + const groupActiveEditorDirtyContext = this.editorPartsView.bind(ActiveEditorDirtyContext, this); + const groupActiveEditorPinnedContext = this.editorPartsView.bind(ActiveEditorPinnedContext, this); + const groupActiveEditorFirstContext = this.editorPartsView.bind(ActiveEditorFirstInGroupContext, this); + const groupActiveEditorLastContext = this.editorPartsView.bind(ActiveEditorLastInGroupContext, this); + const groupActiveEditorStickyContext = this.editorPartsView.bind(ActiveEditorStickyContext, this); + const groupEditorsCountContext = this.editorPartsView.bind(EditorGroupEditorsCountContext, this); + const groupLockedContext = this.editorPartsView.bind(ActiveEditorGroupLockedContext, this); - const groupActiveEditorAvailableEditorIds = ActiveEditorAvailableEditorIdsContext.bindTo(this.scopedContextKeyService); - const groupActiveEditorCanSplitInGroupContext = ActiveEditorCanSplitInGroupContext.bindTo(this.scopedContextKeyService); - const sideBySideEditorContext = SideBySideEditorActiveContext.bindTo(this.scopedContextKeyService); + const multipleEditorsSelectedContext = MultipleEditorsSelectedInGroupContext.bindTo(this.scopedContextKeyService); + const twoEditorsSelectedContext = TwoEditorsSelectedInGroupContext.bindTo(this.scopedContextKeyService); + + const groupActiveEditorContext = this.editorPartsView.bind(ActiveEditorContext, this); + const groupActiveEditorIsReadonly = this.editorPartsView.bind(ActiveEditorReadonlyContext, this); + const groupActiveEditorCanRevert = this.editorPartsView.bind(ActiveEditorCanRevertContext, this); + const groupActiveEditorCanToggleReadonly = this.editorPartsView.bind(ActiveEditorCanToggleReadonlyContext, this); + const groupActiveCompareEditorCanSwap = this.editorPartsView.bind(ActiveCompareEditorCanSwapContext, this); + const groupTextCompareEditorVisibleContext = this.editorPartsView.bind(TextCompareEditorVisibleContext, this); + const groupTextCompareEditorActiveContext = this.editorPartsView.bind(TextCompareEditorActiveContext, this); + + const groupActiveEditorAvailableEditorIds = this.editorPartsView.bind(ActiveEditorAvailableEditorIdsContext, this); + const groupActiveEditorCanSplitInGroupContext = this.editorPartsView.bind(ActiveEditorCanSplitInGroupContext, this); + const groupActiveEditorIsSideBySideEditorContext = this.editorPartsView.bind(SideBySideEditorActiveContext, this); const activeEditorListener = this._register(new MutableDisposable()); @@ -264,22 +278,46 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.scopedContextKeyService.bufferChangeEvents(() => { const activeEditor = this.activeEditor; + const activeEditorPane = this.activeEditorPane; this.resourceContext.set(EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY })); applyAvailableEditorIds(groupActiveEditorAvailableEditorIds, activeEditor, this.editorResolverService); - groupActiveEditorCanSplitInGroupContext.set(activeEditor ? activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup) : false); - sideBySideEditorContext.set(activeEditor?.typeId === SideBySideEditorInput.ID); - if (activeEditor) { + groupActiveEditorCanSplitInGroupContext.set(activeEditor.hasCapability(EditorInputCapabilities.CanSplitInGroup)); + groupActiveEditorIsSideBySideEditorContext.set(activeEditor.typeId === SideBySideEditorInput.ID); + groupActiveEditorDirtyContext.set(activeEditor.isDirty() && !activeEditor.isSaving()); activeEditorListener.value = activeEditor.onDidChangeDirty(() => { groupActiveEditorDirtyContext.set(activeEditor.isDirty() && !activeEditor.isSaving()); }); } else { + groupActiveEditorCanSplitInGroupContext.set(false); + groupActiveEditorIsSideBySideEditorContext.set(false); groupActiveEditorDirtyContext.set(false); } + + if (activeEditorPane) { + groupActiveEditorContext.set(activeEditorPane.getId()); + groupActiveEditorCanRevert.set(!activeEditorPane.input.hasCapability(EditorInputCapabilities.Untitled)); + groupActiveEditorIsReadonly.set(!!activeEditorPane.input.isReadonly()); + + const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY }); + const secondaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.SECONDARY }); + groupActiveCompareEditorCanSwap.set(activeEditorPane.input instanceof DiffEditorInput && !activeEditorPane.input.original.isReadonly() && !!primaryEditorResource && (this.fileService.hasProvider(primaryEditorResource) || primaryEditorResource.scheme === Schemas.untitled) && !!secondaryEditorResource && (this.fileService.hasProvider(secondaryEditorResource) || secondaryEditorResource.scheme === Schemas.untitled)); + groupActiveEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly)); + + const activePaneDiffEditor = activeEditorPane?.getId() === TEXT_DIFF_EDITOR_ID; + groupTextCompareEditorActiveContext.set(activePaneDiffEditor); + groupTextCompareEditorVisibleContext.set(activePaneDiffEditor); + } else { + groupActiveEditorContext.reset(); + groupActiveEditorCanRevert.reset(); + groupActiveEditorIsReadonly.reset(); + groupActiveCompareEditorCanSwap.reset(); + groupActiveEditorCanToggleReadonly.reset(); + } }); }; @@ -296,6 +334,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { groupActiveEditorStickyContext.set(this.model.activeEditor ? this.model.isSticky(this.model.activeEditor) : false); break; case GroupModelChangeKind.EDITOR_CLOSE: + groupActiveEditorPinnedContext.set(this.model.activeEditor ? this.model.isPinned(this.model.activeEditor) : false); + groupActiveEditorStickyContext.set(this.model.activeEditor ? this.model.isSticky(this.model.activeEditor) : false); case GroupModelChangeKind.EDITOR_OPEN: case GroupModelChangeKind.EDITOR_MOVE: groupActiveEditorFirstContext.set(this.model.isFirst(this.model.activeEditor)); @@ -311,6 +351,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { groupActiveEditorStickyContext.set(this.model.isSticky(this.model.activeEditor)); } break; + case GroupModelChangeKind.EDITORS_SELECTION: + multipleEditorsSelectedContext.set(this.model.selectedEditors.length > 1); + twoEditorsSelectedContext.set(this.model.selectedEditors.length === 2); + break; } // Group editors count context @@ -557,8 +601,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Handle within - if (e.kind === GroupModelChangeKind.GROUP_LOCKED) { - this.element.classList.toggle('locked', this.isLocked); + switch (e.kind) { + case GroupModelChangeKind.GROUP_LOCKED: + this.element.classList.toggle('locked', this.isLocked); + break; + case GroupModelChangeKind.EDITORS_SELECTION: + this.onDidChangeEditorSelection(); + break; } if (!e.editor) { @@ -818,6 +867,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.titleControl.updateEditorLabel(editor); } + private onDidChangeEditorSelection(): void { + + // Forward to title control + this.titleControl.updateEditorSelections(); + } + private onDidVisibilityChange(visible: boolean): void { // Forward to active editor pane @@ -890,6 +945,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { setActive(isActive: boolean): void { this.active = isActive; + // Clear selection when group no longer active + if (!isActive && this.activeEditor && this.selectedEditors.length > 1) { + this.setSelection(this.activeEditor, []); + } + // Update container this.element.classList.toggle('active', isActive); this.element.classList.toggle('inactive', !isActive); @@ -936,6 +996,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.activeEditor; } + get selectedEditors(): EditorInput[] { + return this.model.selectedEditors; + } + get previewEditor(): EditorInput | null { return this.model.previewEditor; } @@ -948,6 +1012,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.isSticky(editorOrIndex); } + isSelected(editor: EditorInput): boolean { + return this.model.isSelected(editor); + } + isTransient(editorOrIndex: EditorInput | number): boolean { return this.model.isTransient(editorOrIndex); } @@ -956,6 +1024,17 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.isActive(editor); } + async setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): Promise { + if (!this.isActive(activeSelectedEditor)) { + // The active selected editor is not yet opened, so we go + // through `openEditor` to show it. We pass the inactive + // selection as internal options + await this.openEditor(activeSelectedEditor, { activation: EditorActivation.ACTIVATE }, { inactiveSelection: inactiveSelectedEditors }); + } else { + this.model.setSelection(activeSelectedEditor, inactiveSelectedEditors); + } + } + contains(candidate: EditorInput | IUntypedEditorInput, options?: IMatchEditorOptions): boolean { return this.model.contains(candidate, options); } @@ -1106,6 +1185,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { pinned, sticky: options?.sticky || (typeof options?.index === 'number' && this.model.isSticky(options.index)), transient: !!options?.transient, + inactiveSelection: internalOptions?.inactiveSelection, active: this.count === 0 || !options || !options.inactive, supportSideBySide: internalOptions?.supportSideBySide }; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 77d8a445857..c9ac110bc23 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -62,7 +62,7 @@ export class EditorGroupWatermark extends Disposable { private readonly transientDisposables = this._register(new DisposableStore()); private enabled: boolean = false; private workbenchState: WorkbenchState; - private keybindingLabel?: KeybindingLabel; + private keybindingLabels = new Set(); constructor( container: HTMLElement, @@ -137,6 +137,9 @@ export class EditorGroupWatermark extends Disposable { const update = () => { clearNode(box); + this.keybindingLabels.forEach(label => label.dispose()); + this.keybindingLabels.clear(); + for (const entry of selected) { const keys = this.keybindingService.lookupKeybinding(entry.id); if (!keys) { @@ -146,9 +149,9 @@ export class EditorGroupWatermark extends Disposable { const dt = append(dl, $('dt')); dt.textContent = entry.text; const dd = append(dl, $('dd')); - this.keybindingLabel?.dispose(); - this.keybindingLabel = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); - this.keybindingLabel.set(keys); + const label = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); + label.set(keys); + this.keybindingLabels.add(label); } }; @@ -164,6 +167,6 @@ export class EditorGroupWatermark extends Disposable { override dispose(): void { super.dispose(); this.clear(); - this.keybindingLabel?.dispose(); + this.keybindingLabels.forEach(label => label.dispose()); } } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 1fa0dcbeb39..e467ab0270e 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -978,9 +978,9 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { // Scoped instantiation service const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.container)); - this.scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( + this.scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, scopedContextKeyService] - )); + ))); // Grid control this._willRestoreState = !options || options.restorePreviousState; diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 9a1d52cb34a..574c97e4153 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Emitter } from 'vs/base/common/event'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { EditorPart, IEditorPartUIState, MainEditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupView, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; @@ -20,6 +20,7 @@ import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IAuxiliaryWindowOpenOptions, IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { generateUuid } from 'vs/base/common/uuid'; +import { ContextKeyValue, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; interface IEditorPartsUIState { readonly auxiliary: IAuxiliaryEditorPartState[]; @@ -45,10 +46,11 @@ export class EditorParts extends MultiWindowParts implements IEditor private mostRecentActiveParts = [this.mainPart]; constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IThemeService themeService: IThemeService, - @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService + @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super('workbench.editorParts', themeService, storageService); @@ -60,6 +62,7 @@ export class EditorParts extends MultiWindowParts implements IEditor private registerListeners(): void { this._register(this.onDidChangeMementoValue(StorageScope.WORKSPACE, this._store)(e => this.onDidChangeMementoState(e))); + this.whenReady.then(() => this.registerGroupsContextKeyListeners()); } protected createMainEditorPart(): MainEditorPart { @@ -448,7 +451,7 @@ export class EditorParts extends MultiWindowParts implements IEditor //#endregion - //#region Editor Groups Service + //#region Group Management get activeGroup(): IEditorGroupView { return this.activePart.activeGroup; @@ -629,6 +632,147 @@ export class EditorParts extends MultiWindowParts implements IEditor //#endregion + //#region Editor Group Context Key Handling + + private readonly globalContextKeys = new Map>(); + private readonly scopedContextKeys = new Map>>(); + + private registerGroupsContextKeyListeners(): void { + this._register(this.onDidChangeActiveGroup(() => this.updateGlobalContextKeys())); + this.groups.forEach(group => this.registerGroupContextKeyProvidersListeners(group)); + this._register(this.onDidAddGroup(group => this.registerGroupContextKeyProvidersListeners(group))); + this._register(this.onDidRemoveGroup(group => { + this.scopedContextKeys.delete(group.id); + this.registeredContextKeys.delete(group.id); + this.contextKeyProviderDisposables.deleteAndDispose(group.id); + })); + } + + private updateGlobalContextKeys(): void { + const activeGroupScopedContextKeys = this.scopedContextKeys.get(this.activeGroup.id); + if (!activeGroupScopedContextKeys) { + return; + } + + for (const [key, globalContextKey] of this.globalContextKeys) { + const scopedContextKey = activeGroupScopedContextKeys.get(key); + if (scopedContextKey) { + globalContextKey.set(scopedContextKey.get()); + } else { + globalContextKey.reset(); + } + } + } + + bind(contextKey: RawContextKey, group: IEditorGroupView): IContextKey { + + // Ensure we only bind to the same context key once globaly + let globalContextKey = this.globalContextKeys.get(contextKey.key); + if (!globalContextKey) { + globalContextKey = contextKey.bindTo(this.contextKeyService); + this.globalContextKeys.set(contextKey.key, globalContextKey); + } + + // Ensure we only bind to the same context key once per group + let groupScopedContextKeys = this.scopedContextKeys.get(group.id); + if (!groupScopedContextKeys) { + groupScopedContextKeys = new Map>(); + this.scopedContextKeys.set(group.id, groupScopedContextKeys); + } + let scopedContextKey = groupScopedContextKeys.get(contextKey.key); + if (!scopedContextKey) { + scopedContextKey = contextKey.bindTo(group.scopedContextKeyService); + groupScopedContextKeys.set(contextKey.key, scopedContextKey); + } + + const that = this; + return { + get(): T | undefined { + return scopedContextKey.get() as T | undefined; + }, + set(value: T): void { + if (that.activeGroup === group) { + globalContextKey.set(value); + } + scopedContextKey.set(value); + }, + reset(): void { + if (that.activeGroup === group) { + globalContextKey.reset(); + } + scopedContextKey.reset(); + }, + }; + } + + private readonly contextKeyProviders = new Map>(); + private readonly registeredContextKeys = new Map>(); + + registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): IDisposable { + if (this.contextKeyProviders.has(provider.contextKey.key) || this.globalContextKeys.has(provider.contextKey.key)) { + throw new Error(`A context key provider for key ${provider.contextKey.key} already exists.`); + } + + this.contextKeyProviders.set(provider.contextKey.key, provider); + + const setContextKeyForGroups = () => { + for (const group of this.groups) { + this.updateRegisteredContextKey(group, provider); + } + }; + + // Run initially and on change + setContextKeyForGroups(); + const onDidChange = provider.onDidChange?.(() => setContextKeyForGroups()); + + return toDisposable(() => { + onDidChange?.dispose(); + + this.globalContextKeys.delete(provider.contextKey.key); + this.scopedContextKeys.forEach(scopedContextKeys => scopedContextKeys.delete(provider.contextKey.key)); + + this.contextKeyProviders.delete(provider.contextKey.key); + this.registeredContextKeys.forEach(registeredContextKeys => registeredContextKeys.delete(provider.contextKey.key)); + }); + } + + private readonly contextKeyProviderDisposables = this._register(new DisposableMap()); + private registerGroupContextKeyProvidersListeners(group: IEditorGroupView): void { + + // Update context keys from providers for the group when its active editor changes + const disposable = group.onDidActiveEditorChange(() => { + for (const contextKeyProvider of this.contextKeyProviders.values()) { + this.updateRegisteredContextKey(group, contextKeyProvider); + } + }); + + this.contextKeyProviderDisposables.set(group.id, disposable); + } + + private updateRegisteredContextKey(group: IEditorGroupView, provider: IEditorGroupContextKeyProvider): void { + + // Get the group scoped context keys for the provider + // If the providers context key has not yet been bound + // to the group, do so now. + + let groupRegisteredContextKeys = this.registeredContextKeys.get(group.id); + if (!groupRegisteredContextKeys) { + groupRegisteredContextKeys = new Map(); + this.scopedContextKeys.set(group.id, groupRegisteredContextKeys); + } + + let scopedRegisteredContextKey = groupRegisteredContextKeys.get(provider.contextKey.key); + if (!scopedRegisteredContextKey) { + scopedRegisteredContextKey = this.bind(provider.contextKey, group); + groupRegisteredContextKeys.set(provider.contextKey.key, scopedRegisteredContextKey); + } + + // Set the context key value for the group context + scopedRegisteredContextKey.set(provider.getGroupContextKeyValue(group)); + } + + //#endregion + //#region Main Editor Part Only get partOptions() { return this.mainPart.partOptions; } diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index dc58c035fed..c2ceeb2d7d7 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -893,9 +893,9 @@ export class EditorStatusContribution extends Disposable implements IWorkbenchCo super(); // Main Editor Status - const mainInstantiationService = instantiationService.createChild(new ServiceCollection( + const mainInstantiationService = this._register(instantiationService.createChild(new ServiceCollection( [IEditorService, editorService.createScoped('main', this._store)] - )); + ))); this._register(mainInstantiationService.createInstance(EditorStatus, mainWindow.vscodeWindowId)); // Auxiliary Editor Status diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index d7a82ed5c16..e5e4f7774de 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -86,6 +86,7 @@ export interface IEditorTabsControl extends IDisposable { stickEditor(editor: EditorInput): void; unstickEditor(editor: EditorInput): void; setActive(isActive: boolean): void; + updateEditorSelections(): void; updateEditorLabel(editor: EditorInput): void; updateEditorDirty(editor: EditorInput): void; layout(dimensions: IEditorTitleControlDimensions): Dimension; @@ -145,9 +146,9 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC super(themeService); this.contextMenuContextKeyService = this._register(this.contextKeyService.createScoped(parent)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection( + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IContextKeyService, this.contextMenuContextKeyService], - )); + ))); this.resourceContext = this._register(scopedInstantiationService.createInstance(ResourceContextKey)); @@ -502,6 +503,8 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC abstract setActive(isActive: boolean): void; + abstract updateEditorSelections(): void; + abstract updateEditorLabel(editor: EditorInput): void; abstract updateEditorDirty(editor: EditorInput): void; diff --git a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts index 5ff3995ffbd..d134e9b6175 100644 --- a/src/vs/workbench/browser/parts/editor/editorTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTitleControl.ts @@ -163,6 +163,10 @@ export class EditorTitleControl extends Themable { return this.editorTabsControl.setActive(isActive); } + updateEditorSelections(): void { + this.editorTabsControl.updateEditorSelections(); + } + updateEditorLabel(editor: EditorInput): void { return this.editorTabsControl.updateEditorLabel(editor); } diff --git a/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css index bf44c02aee2..24de0a1f384 100644 --- a/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css @@ -46,4 +46,12 @@ border-radius: 10px; font-size: 12px; position: absolute; + /* + * Browsers apply an effect to the drag image when the div becomes too + * large which makes them unreadable. Use max width so it does not happen + */ + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index 557a5ff4b0f..93559402aa5 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -134,6 +134,11 @@ color: var(--vscode-tab-activeForeground); } +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.selected:not(.active) { + background-color: var(--vscode-tab-selectedBackground); + color: var(--vscode-tab-selectedForeground); +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.active) { box-shadow: none; } @@ -269,6 +274,7 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected.tab-border-top > .tab-border-top-container, .monaco-workbench .part.editor > .content .editor-group-container > .title:not(.two-tab-bars) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, .monaco-workbench .part.editor > .content .editor-group-container > .title.two-tab-bars .tabs-and-actions-container:not(:first-child) .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { @@ -279,7 +285,8 @@ width: 100%; } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container { +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected.tab-border-top > .tab-border-top-container { z-index: 6; top: 0; height: 1px; diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index 706924c23e7..395c83aa7a1 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -26,8 +26,8 @@ import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElemen import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { getOrSet } from 'vs/base/common/map'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER } from 'vs/workbench/common/theme'; -import { activeContrastBorder, contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; +import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER, TAB_SELECTED_BORDER_TOP } from 'vs/workbench/common/theme'; +import { activeContrastBorder, contrastBorder, editorBackground, listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, extractTreeDropData, isWindowDraggedOver } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -56,6 +56,8 @@ import { IEditorTitleControlDimensions } from 'vs/workbench/browser/parts/editor import { StickyEditorGroupModel, UnstickyEditorGroupModel } from 'vs/workbench/common/editor/filteredEditorGroupModel'; import { IReadonlyEditorGroupModel } from 'vs/workbench/common/editor/editorGroupModel'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { BugIndicatingError } from 'vs/base/common/errors'; +import { applyDragImage } from 'vs/base/browser/dnd'; interface IEditorInputLabel { readonly editor: EditorInput; @@ -674,7 +676,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Activity has an impact on each tab's active indication this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { - this.redrawTabActiveAndDirty(isGroupActive, editor, tabContainer, tabActionBar); + this.redrawTabSelectedActiveAndDirty(isGroupActive, editor, tabContainer, tabActionBar); }); // Activity has an impact on the toolbar, so we need to update and layout @@ -682,6 +684,12 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.layout(this.dimensions, { forceRevealActiveTab: true }); } + updateEditorSelections(): void { + this.forEachTab((editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => { + this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); + }); + } + private updateEditorLabelScheduler = this._register(new RunOnceScheduler(() => this.doUpdateEditorLabels(), 0)); updateEditorLabel(editor: EditorInput): void { @@ -709,7 +717,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } updateEditorDirty(editor: EditorInput): void { - this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar)); + this.withTab(editor, (editor, tabIndex, tabContainer, tabLabelWidget, tabLabel, tabActionBar) => this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar)); } override updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void { @@ -725,6 +733,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.updateTabsScrollbarSizing(); } + // Update editor actions + if (oldOptions.alwaysShowEditorActions !== newOptions.alwaysShowEditorActions) { + this.updateEditorActionsToolbar(); + } + // Update tabs sizing if ( oldOptions.tabSizingFixedMinWidth !== newOptions.tabSizingFixedMinWidth || @@ -853,10 +866,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { return this.groupView.getIndexOfEditor(editor); } + private lastSingleSelectSelectedEditor: EditorInput | undefined; private registerTabListeners(tab: HTMLElement, tabIndex: number, tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): IDisposable { const disposables = new DisposableStore(); - const handleClickOrTouch = (e: MouseEvent | GestureEvent, preserveFocus: boolean): void => { + const handleClickOrTouch = async (e: MouseEvent | GestureEvent, preserveFocus: boolean): Promise => { tab.blur(); // prevent flicker of focus outline on tab until editor got focus if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) { @@ -864,7 +878,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { e.preventDefault(); // required to prevent auto-scrolling (https://github.com/microsoft/vscode/issues/16690) } - return undefined; + return; } if (this.originatesFromTabActionBar(e)) { @@ -874,11 +888,34 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Open tabs editor const editor = this.tabsModel.getEditorByIndex(tabIndex); if (editor) { - // Even if focus is preserved make sure to activate the group. - this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }); + if (e.shiftKey) { + let anchor: EditorInput; + if (this.lastSingleSelectSelectedEditor && this.tabsModel.isSelected(this.lastSingleSelectSelectedEditor)) { + // The last selected editor is the anchor + anchor = this.lastSingleSelectSelectedEditor; + } else { + // The active editor is the anchor + const activeEditor = assertIsDefined(this.groupView.activeEditor); + this.lastSingleSelectSelectedEditor = activeEditor; + anchor = activeEditor; + } + await this.selectEditorsBetween(editor, anchor); + } else if ((e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh)) { + if (this.tabsModel.isSelected(editor)) { + await this.unselectEditor(editor); + } else { + await this.selectEditor(editor); + this.lastSingleSelectSelectedEditor = editor; + } + } else { + // Even if focus is preserved make sure to activate the group. + // If a new active editor is selected, keep the current selection on key + // down such that drag and drop can operate over the selection. The selection + // is removed on key up in this case. + const inactiveSelection = this.tabsModel.isSelected(editor) ? this.groupView.selectedEditors.filter(e => !e.matches(editor)) : []; + await this.groupView.openEditor(editor, { preserveFocus, activation: EditorActivation.ACTIVATE }, { inactiveSelection, focusTabControl: true }); + } } - - return undefined; }; const showContextMenu = (e: Event) => { @@ -899,11 +936,24 @@ export class MultiEditorTabsControl extends EditorTabsControl { tabsScrollbar.setScrollPosition({ scrollLeft: tabsScrollbar.getScrollPosition().scrollLeft - e.translationX }); })); - // Prevent flicker of focus outline on tab until editor got focus - disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, e => { + // Update selection & prevent flicker of focus outline on tab until editor got focus + disposables.add(addDisposableListener(tab, EventType.MOUSE_UP, async e => { EventHelper.stop(e); tab.blur(); + + if (isMouseEvent(e) && (e.button !== 0 /* middle/right mouse button */ || (isMacintosh && e.ctrlKey /* macOS context menu */))) { + return; + } + + if (this.originatesFromTabActionBar(e)) { + return; // not when clicking on actions + } + + const isCtrlCmd = (e.ctrlKey && !isMacintosh) || (e.metaKey && isMacintosh); + if (!isCtrlCmd && !e.shiftKey && this.groupView.selectedEditors.length > 1) { + await this.unselectAllEditors(); + } })); // Close on mouse middle click @@ -1029,12 +1079,17 @@ export class MultiEditorTabsControl extends EditorTabsControl { } isNewWindowOperation = this.isNewWindowOperation(e); - - this.editorTransfer.setData([new DraggedEditorIdentifier({ editor, groupId: this.groupView.id })], DraggedEditorIdentifier.prototype); + const selectedEditors = this.groupView.selectedEditors; + this.editorTransfer.setData(selectedEditors.map(e => new DraggedEditorIdentifier({ editor: e, groupId: this.groupView.id })), DraggedEditorIdentifier.prototype); if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'copyMove'; - e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position to make room for drop-border feedback + if (selectedEditors.length > 1) { + const label = `${editor.getName()} + ${selectedEditors.length - 1}`; + applyDragImage(e, label, 'monaco-editor-group-drag-image', this.getColor(listActiveSelectionBackground), this.getColor(listActiveSelectionForeground)); + } else { + e.dataTransfer.setDragImage(tab, 0, 0); // top left corner of dragged tab set to cursor position to make room for drop-border feedback + } } // Apply some datatransfer types to allow for dragging the element outside of the application @@ -1082,14 +1137,14 @@ export class MultiEditorTabsControl extends EditorTabsControl { onDragEnd: async e => { this.updateDropFeedback(tab, false, e, tabIndex); - + const draggedEditors = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); - const editor = this.tabsModel.getEditorByIndex(tabIndex); if ( !isNewWindowOperation || isWindowDraggedOver() || - !editor + !draggedEditors || + draggedEditors.length === 0 ) { return; // drag to open in new window is disabled } @@ -1100,10 +1155,11 @@ export class MultiEditorTabsControl extends EditorTabsControl { } const targetGroup = auxiliaryEditorPart.activeGroup; - if (this.isMoveOperation(lastDragEvent ?? e, targetGroup.id, editor)) { - this.groupView.moveEditor(editor, targetGroup); + const editors = draggedEditors.map(de => ({ editor: de.identifier.editor })); + if (this.isMoveOperation(lastDragEvent ?? e, targetGroup.id, draggedEditors[0].identifier.editor)) { + this.groupView.moveEditors(editors, targetGroup); } else { - this.groupView.copyEditor(editor, targetGroup); + this.groupView.copyEditors(editors, targetGroup); } targetGroup.focus(); @@ -1118,22 +1174,6 @@ export class MultiEditorTabsControl extends EditorTabsControl { targetIndex++; } - // If we are moving an editor inside the same group and it is - // located before the target index we need to reduce the index - // by one to account for the fact that the move will cause all - // subsequent tabs to move one to the left. - const editorIdentifiers = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (editorIdentifiers !== undefined) { - const draggedEditorIdentifier = editorIdentifiers[0].identifier; - const sourceGroup = this.editorPartsView.getGroup(draggedEditorIdentifier.groupId); - if (sourceGroup?.id === this.groupView.id) { - const editorIndex = sourceGroup.getIndexOfEditor(draggedEditorIdentifier.editor); - if (editorIndex < targetIndex) { - targetIndex--; - } - } - } - this.onDrop(e, targetIndex, tabsContainer); } })); @@ -1144,7 +1184,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { private isSupportedDropTransfer(e: DragEvent): boolean { if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const group = data[0]; if (group.identifier === this.groupView.id) { return false; // groups cannot be dropped on group it originates from @@ -1234,6 +1274,93 @@ export class MultiEditorTabsControl extends EditorTabsControl { return { leftElement: tabBefore as HTMLElement, rightElement: tabAfter as HTMLElement }; } + private async selectEditor(editor: EditorInput): Promise { + if (this.groupView.isActive(editor)) { + return; + } + + await this.groupView.setSelection(editor, this.groupView.selectedEditors); + } + + private async selectEditorsBetween(target: EditorInput, anchor: EditorInput): Promise { + const editorIndex = this.groupView.getIndexOfEditor(target); + if (editorIndex === -1) { + throw new BugIndicatingError(); + } + + const anchorIndex = this.groupView.getIndexOfEditor(anchor); + if (anchorIndex === -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; + + if (!this.tabsModel.isSelected(currentIndex)) { + break; + } + + const currentEditor = this.groupView.getEditorByIndex(currentIndex); + if (!currentEditor) { + break; + } + + selection = selection.filter(editor => !editor.matches(currentEditor)); + } + + // Select editors between anchor and target + const fromIndex = anchorIndex < editorIndex ? anchorIndex : editorIndex; + const toIndex = anchorIndex < editorIndex ? editorIndex : anchorIndex; + + const editorsToSelect = this.groupView.getEditors(EditorsOrder.SEQUENTIAL).slice(fromIndex, toIndex + 1); + for (const editor of editorsToSelect) { + if (!this.tabsModel.isSelected(editor)) { + selection.push(editor); + } + } + + const inactiveSelectedEditors = selection.filter(editor => !editor.matches(target)); + await this.groupView.setSelection(target, inactiveSelectedEditors); + } + + private async unselectEditor(editor: EditorInput): Promise { + const isUnselectingActiveEditor = this.groupView.isActive(editor); + + // If there is only one editor selected, do not unselect it + if (isUnselectingActiveEditor && this.groupView.selectedEditors.length === 1) { + return; + } + + let newActiveEditor = assertIsDefined(this.groupView.activeEditor); + + // If active editor is bing unselected then find the most recently opened selected editor + // that is not the editor being unselected + if (isUnselectingActiveEditor) { + 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)) { + newActiveEditor = recentEditor; + break; + } + } + } + + const inactiveSelectedEditors = this.groupView.selectedEditors.filter(e => !e.matches(editor) && !e.matches(newActiveEditor)); + await this.groupView.setSelection(newActiveEditor, inactiveSelectedEditors); + } + + private async unselectAllEditors(): Promise { + if (this.groupView.selectedEditors.length > 1) { + const activeEditor = assertIsDefined(this.groupView.activeEditor); + await this.groupView.setSelection(activeEditor, []); + } + } + private computeTabLabels(): void { const { labelFormat } = this.groupsView.partOptions; const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat); @@ -1451,8 +1578,8 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Borders / outline this.redrawTabBorders(tabIndex, tabContainer); - // Active / dirty state - this.redrawTabActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); + // Selection / active / dirty state + this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar); } private redrawTabLabel(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { @@ -1510,7 +1637,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { } } - private redrawTabActiveAndDirty(isGroupActive: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { + private redrawTabSelectedActiveAndDirty(isGroupActive: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { const isTabActive = this.tabsModel.isActive(editor); const hasModifiedBorderTop = this.doRedrawTabDirty(isGroupActive, isTabActive, editor, tabContainer); @@ -1518,25 +1645,36 @@ export class MultiEditorTabsControl extends EditorTabsControl { } private doRedrawTabActive(isGroupActive: boolean, allowBorderTop: boolean, editor: EditorInput, tabContainer: HTMLElement, tabActionBar: ActionBar): void { - const isActive = this.tabsModel.isActive(editor); + const isSelected = this.tabsModel.isSelected(editor); tabContainer.classList.toggle('active', isActive); + tabContainer.classList.toggle('selected', isSelected); tabContainer.setAttribute('aria-selected', isActive ? 'true' : 'false'); tabContainer.tabIndex = isActive ? 0 : -1; // Only active tab can be focused into tabActionBar.setFocusable(isActive); + // Set border BOTTOM if theme defined color if (isActive) { - // Set border BOTTOM if theme defined color const activeTabBorderColorBottom = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER : TAB_UNFOCUSED_ACTIVE_BORDER); tabContainer.classList.toggle('tab-border-bottom', !!activeTabBorderColorBottom); - tabContainer.style.setProperty('--tab-border-bottom-color', activeTabBorderColorBottom?.toString() ?? ''); - - // Set border TOP if theme defined color - const activeTabBorderColorTop = allowBorderTop ? this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP) : undefined; - tabContainer.classList.toggle('tab-border-top', !!activeTabBorderColorTop); - tabContainer.style.setProperty('--tab-border-top-color', activeTabBorderColorTop?.toString() ?? ''); + tabContainer.style.setProperty('--tab-border-bottom-color', activeTabBorderColorBottom ?? ''); } + + // Set border TOP if theme defined color + let tabBorderColorTop: string | null = null; + if (allowBorderTop) { + if (isActive) { + tabBorderColorTop = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER_TOP : TAB_UNFOCUSED_ACTIVE_BORDER_TOP); + } + + if (tabBorderColorTop === null && isSelected) { + tabBorderColorTop = this.getColor(TAB_SELECTED_BORDER_TOP); + } + } + + tabContainer.classList.toggle('tab-border-top', !!tabBorderColorTop); + tabContainer.style.setProperty('--tab-border-top-color', tabBorderColorTop ?? ''); } private doRedrawTabDirty(isGroupActive: boolean, isTabActive: boolean, editor: EditorInput, tabContainer: HTMLElement): boolean { @@ -1602,7 +1740,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Inactive: only show "Unlock" and secondary actions else { return { - primary: editorActions.primary.filter(action => action.id === UNLOCK_GROUP_COMMAND_ID), + primary: this.groupsView.partOptions.alwaysShowEditorActions ? editorActions.primary : editorActions.primary.filter(action => action.id === UNLOCK_GROUP_COMMAND_ID), secondary: editorActions.secondary }; } @@ -2053,7 +2191,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { this.updateDropFeedback(tabsContainer, false, e, targetTabIndex); tabsContainer.classList.remove('scroll'); - const targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex; + let targetEditorIndex = this.tabsModel instanceof UnstickyEditorGroupModel ? targetTabIndex + this.groupView.stickyCount : targetTabIndex; const options: IEditorOptions = { sticky: this.tabsModel instanceof StickyEditorGroupModel && this.tabsModel.stickyCount === targetEditorIndex, index: targetEditorIndex @@ -2062,7 +2200,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for group transfer if (this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype)) { const data = this.groupTransfer.getData(DraggedEditorGroupIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const sourceGroup = this.editorPartsView.getGroup(data[0].identifier); if (sourceGroup) { const mergeGroupOptions: IMergeGroupOptions = { index: targetEditorIndex }; @@ -2081,31 +2219,42 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for editor transfer else if (this.editorTransfer.hasData(DraggedEditorIdentifier.prototype)) { const data = this.editorTransfer.getData(DraggedEditorIdentifier.prototype); - if (Array.isArray(data)) { - const draggedEditor = data[0].identifier; - const sourceGroup = this.editorPartsView.getGroup(draggedEditor.groupId); + if (Array.isArray(data) && data.length > 0) { + const sourceGroup = this.editorPartsView.getGroup(data[0].identifier.groupId); if (sourceGroup) { + for (const de of data) { + const editor = de.identifier.editor; - // Move editor to target position and index - if (this.isMoveOperation(e, draggedEditor.groupId, draggedEditor.editor)) { - sourceGroup.moveEditor(draggedEditor.editor, this.groupView, options); - } + // Only allow moving/copying from a single group source + if (sourceGroup.id !== de.identifier.groupId) { + continue; + } - // Copy editor to target position and index - else { - sourceGroup.copyEditor(draggedEditor.editor, this.groupView, options); + // Keep the same order when moving / copying editors within the same group + const sourceEditorIndex = sourceGroup.getIndexOfEditor(editor); + if (sourceGroup === this.groupView && sourceEditorIndex < targetEditorIndex) { + targetEditorIndex--; + } + + if (this.isMoveOperation(e, de.identifier.groupId, editor)) { + sourceGroup.moveEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); + } else { + sourceGroup.copyEditor(editor, this.groupView, { ...options, index: targetEditorIndex }); + } + + targetEditorIndex++; } } - - this.groupView.focus(); - this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); } + + this.groupView.focus(); + this.editorTransfer.clearData(DraggedEditorIdentifier.prototype); } // Check for tree items else if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) { const data = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype); - if (Array.isArray(data)) { + if (Array.isArray(data) && data.length > 0) { const editors: IUntypedEditorInput[] = []; for (const id of data) { const dataTransferItem = await this.treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier); @@ -2157,6 +2306,11 @@ registerThemingParticipant((theme, collector) => { outline-offset: -5px; } + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.selected:not(.active):not(:hover) { + outline: 1px dotted; + outline-offset: -5px; + } + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:focus { outline-style: dashed; } @@ -2195,7 +2349,7 @@ registerThemingParticipant((theme, collector) => { const tabHoverBackground = theme.getColor(TAB_HOVER_BACKGROUND); if (tabHoverBackground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:not(.selected):hover { background-color: ${tabHoverBackground} !important; } `); @@ -2204,7 +2358,7 @@ registerThemingParticipant((theme, collector) => { const tabUnfocusedHoverBackground = theme.getColor(TAB_UNFOCUSED_HOVER_BACKGROUND); if (tabUnfocusedHoverBackground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.selected):hover { background-color: ${tabUnfocusedHoverBackground} !important; } `); @@ -2214,7 +2368,7 @@ registerThemingParticipant((theme, collector) => { const tabHoverForeground = theme.getColor(TAB_HOVER_FOREGROUND); if (tabHoverForeground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:not(.selected):hover { color: ${tabHoverForeground} !important; } `); @@ -2223,7 +2377,7 @@ registerThemingParticipant((theme, collector) => { const tabUnfocusedHoverForeground = theme.getColor(TAB_UNFOCUSED_HOVER_FOREGROUND); if (tabUnfocusedHoverForeground) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover { + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:not(.selected):hover { color: ${tabUnfocusedHoverForeground} !important; } `); diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 24d85415eb8..83823d3ec8f 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -163,6 +163,11 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.setActive(isActive); } + updateEditorSelections(): void { + this.stickyEditorTabsControl.updateEditorSelections(); + this.unstickyEditorTabsControl.updateEditorSelections(); + } + updateEditorLabel(editor: EditorInput): void { this.getEditorTabsController(editor).updateEditorLabel(editor); } @@ -194,7 +199,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont return this.stickyEditorTabsControl.getHeight() + this.unstickyEditorTabsControl.getHeight(); } - public override dispose(): void { + override dispose(): void { this.parent.classList.toggle('two-tab-bars', false); super.dispose(); diff --git a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts index baf08bb4504..6a0859cd3ec 100644 --- a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts @@ -61,6 +61,8 @@ export class NoEditorTabsControl extends EditorTabsControl { setActive(isActive: boolean): void { } + updateEditorSelections(): void { } + updateEditorLabel(editor: EditorInput): void { } updateEditorDirty(editor: EditorInput): void { } diff --git a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts index c33305205c2..b6b3dd436ac 100644 --- a/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/singleEditorTabsControl.ts @@ -179,18 +179,16 @@ export class SingleEditorTabsControl extends EditorTabsControl { this.ifEditorIsActive(editor, () => this.redraw()); } - stickEditor(editor: EditorInput): void { - // Sticky editors are not presented any different with tabs disabled - } + stickEditor(editor: EditorInput): void { } - unstickEditor(editor: EditorInput): void { - // Sticky editors are not presented any different with tabs disabled - } + unstickEditor(editor: EditorInput): void { } setActive(isActive: boolean): void { this.redraw(); } + updateEditorSelections(): void { } + updateEditorLabel(editor: EditorInput): void { this.ifEditorIsActive(editor, () => this.redraw()); } @@ -353,7 +351,7 @@ export class SingleEditorTabsControl extends EditorTabsControl { // Inactive: only show "Close, "Unlock" and secondary actions else { return { - primary: editorActions.primary.filter(action => action.id === CLOSE_EDITOR_COMMAND_ID || action.id === UNLOCK_GROUP_COMMAND_ID), + primary: this.groupsView.partOptions.alwaysShowEditorActions ? editorActions.primary : editorActions.primary.filter(action => action.id === CLOSE_EDITOR_COMMAND_ID || action.id === UNLOCK_GROUP_COMMAND_ID), secondary: editorActions.secondary }; } diff --git a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts new file mode 100644 index 00000000000..066afd7adda --- /dev/null +++ b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { IAccessibleViewService, AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation, alertAccessibleViewFocusChange } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IAccessibilitySignalService, AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IListService, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { getNotificationFromContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; +import { NotificationFocusedContext } from 'vs/workbench/common/contextkeys'; +import { INotificationViewItem } from 'vs/workbench/common/notifications'; + +export class NotificationAccessibleView implements IAccessibleViewImplentation { + readonly priority = 90; + readonly name = 'notifications'; + readonly when = NotificationFocusedContext; + readonly type = AccessibleViewType.View; + getProvider(accessor: ServicesAccessor) { + const accessibleViewService = accessor.get(IAccessibleViewService); + const listService = accessor.get(IListService); + const commandService = accessor.get(ICommandService); + const accessibilitySignalService = accessor.get(IAccessibilitySignalService); + + function getProvider() { + const notification = getNotificationFromContext(listService); + if (!notification) { + return; + } + commandService.executeCommand('notifications.showList'); + let notificationIndex: number | undefined; + let length: number | undefined; + const list = listService.lastFocusedList; + if (list instanceof WorkbenchList) { + notificationIndex = list.indexOf(notification); + length = list.length; + } + if (notificationIndex === undefined) { + return; + } + + function focusList(): void { + commandService.executeCommand('notifications.showList'); + if (list && notificationIndex !== undefined) { + list.domFocus(); + try { + list.setFocus([notificationIndex]); + } catch { } + } + } + const message = notification.message.original.toString(); + if (!message) { + return; + } + notification.onDidClose(() => accessibleViewService.next()); + return { + id: AccessibleViewProviderId.Notification, + provideContent: () => { + return notification.source ? localize('notification.accessibleViewSrc', '{0} Source: {1}', message, notification.source) : localize('notification.accessibleView', '{0}', message); + }, + onClose(): void { + focusList(); + }, + next(): void { + if (!list) { + return; + } + focusList(); + list.focusNext(); + alertAccessibleViewFocusChange(notificationIndex, length, 'next'); + getProvider(); + }, + previous(): void { + if (!list) { + return; + } + focusList(); + list.focusPrevious(); + alertAccessibleViewFocusChange(notificationIndex, length, 'previous'); + getProvider(); + }, + verbositySettingKey: 'accessibility.verbosity.notification', + options: { type: AccessibleViewType.View }, + actions: getActionsFromNotification(notification, accessibilitySignalService) + }; + } + return getProvider(); + } +} + + +function getActionsFromNotification(notification: INotificationViewItem, accessibilitySignalService: IAccessibilitySignalService): IAction[] | undefined { + let actions = undefined; + if (notification.actions) { + actions = []; + if (notification.actions.primary) { + actions.push(...notification.actions.primary); + } + if (notification.actions.secondary) { + actions.push(...notification.actions.secondary); + } + } + if (actions) { + for (const action of actions) { + action.class = ThemeIcon.asClassName(Codicon.bell); + const initialAction = action.run; + action.run = () => { + initialAction(); + notification.close(); + }; + } + } + const manageExtension = actions?.find(a => a.label.includes('Manage Extension')); + if (manageExtension) { + manageExtension.class = ThemeIcon.asClassName(Codicon.gear); + } + if (actions) { + actions.push({ + id: 'clearNotification', label: localize('clearNotification', "Clear Notification"), tooltip: localize('clearNotification', "Clear Notification"), run: () => { + notification.close(); + accessibilitySignalService.playSignal(AccessibilitySignal.clear); + }, enabled: true, class: ThemeIcon.asClassName(Codicon.clearAll) + }); + } + return actions; +} + diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 6f205b9d81f..92b1b45d184 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -311,7 +311,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl })); picker.canSelectMany = true; - picker.placeholder = localize('selectSources', "Select sources to enable notifications for"); + picker.placeholder = localize('selectSources', "Select sources to enable all notifications from"); picker.selectedItems = picker.items.filter(item => (item as INotificationSourceFilter).filter === NotificationsFilter.OFF) as (IQuickPickItem & INotificationSourceFilter)[]; picker.show(); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index a073b11dc83..65ad1afe6cc 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -249,7 +249,7 @@ export class NotificationRenderer implements IListRenderer that.notificationService.setFilter({ ...source, filter: isSourceFiltered ? NotificationsFilter.OFF : NotificationsFilter.ERROR }) })); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 45cf1b15b1c..becd185fd60 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -61,7 +61,6 @@ import { Extensions, ITreeItem, ITreeItemLabel, ITreeView, ITreeViewDataProvider import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; -import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; import { CodeDataTransfers, LocalSelectionTransfer } from 'vs/platform/dnd/browser/dnd'; import { toExternalVSDataTransfer } from 'vs/editor/browser/dnd'; import { CheckboxStateHandler, TreeItemCheckbox } from 'vs/workbench/browser/parts/views/checkbox'; @@ -1168,7 +1167,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer(ConfigurationExtensions.Con 'markdownDescription': localize('editorActionsLocation', "Controls where the editor actions are shown."), 'default': 'default' }, + 'workbench.editor.alwaysShowEditorActions': { + 'type': 'boolean', + 'markdownDescription': localize('alwaysShowEditorActions', "Controls whether to always show the editor actions, even when the editor group is not active."), + 'default': false + }, 'workbench.editor.wrapTabs': { 'type': 'boolean', 'markdownDescription': localize({ comment: ['{0}, {1} will be a setting name rendered as a link'], key: 'wrapTabs' }, "Controls whether tabs should be wrapped over multiple lines when exceeding available space or whether a scrollbar should appear instead. This value is ignored when {0} is not set to '{1}'.", '`#workbench.editor.showTabs#`', '`multiple`'), @@ -94,14 +99,14 @@ const registry = Registry.as(ConfigurationExtensions.Con [CustomEditorLabelService.SETTING_ID_PATTERNS]: { 'type': 'object', 'markdownDescription': (() => { - 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. 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:"); + 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. `root/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=1: root/folder/file.txt -> root`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: root/folder/file.txt -> root`). 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.filename', "`${filename}`: name of the file without the file extension (e.g. `root/folder/file.txt -> file`)."), - localize('workbench.editor.label.extname', "`${extname}`: the file extension (e.g. `root/folder/file.txt -> txt`)."), + 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.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`)."), ].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 `root/static/folder/file.html` as `file - folder (html)`."); + 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)`."); return customEditorLabelDescription; })(), diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 3678a7f6286..b0688133537 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -48,6 +48,8 @@ import { setHoverDelegateFactory } from 'vs/base/browser/ui/hover/hoverDelegateF import { setBaseLayerHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate2'; import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler'; import { setProgressAcccessibilitySignalScheduler } from 'vs/base/browser/ui/progressbar/progressAccessibilitySignal'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { NotificationAccessibleView } from 'vs/workbench/browser/parts/notifications/notificationAccessibleView'; export interface IWorkbenchOptions { @@ -416,6 +418,9 @@ export class Workbench extends Layout { // Register Commands registerNotificationCommands(notificationsCenter, notificationsToasts, notificationService.model); + // Register notification accessible view + AccessibleViewRegistry.register(new NotificationAccessibleView()); + // Register with Layout this.registerNotifications({ onDidChangeNotificationsVisibility: Event.map(Event.any(notificationsToasts.onDidChangeVisibility, notificationsCenter.onDidChangeVisibility), () => notificationsToasts.isVisible || notificationsCenter.isVisible) diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 1c82f8542c8..97937218bbb 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -72,6 +72,8 @@ export const ActiveEditorGroupLastContext = new RawContextKey('activeEd export const ActiveEditorGroupLockedContext = new RawContextKey('activeEditorGroupLocked', false, localize('activeEditorGroupLocked', "Whether the active editor group is locked")); export const MultipleEditorGroupsContext = new RawContextKey('multipleEditorGroups', false, localize('multipleEditorGroups', "Whether there are multiple editor groups opened")); 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")); // 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/editor.ts b/src/vs/workbench/common/editor.ts index bd2c42d8510..02aff158f4f 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1079,6 +1079,12 @@ export interface IEditorCommandsContext { preserveFocus?: boolean; } +export function isEditorCommandsContext(context: unknown): context is IEditorCommandsContext { + const candidate = context as IEditorCommandsContext | undefined; + + return typeof candidate?.groupId === 'number'; +} + /** * More information around why an editor was closed in the model. */ @@ -1162,6 +1168,9 @@ export const enum GroupModelChangeKind { GROUP_LABEL, GROUP_LOCKED, + /* Editors Change */ + EDITORS_SELECTION, + /* Editor Changes */ EDITOR_OPEN, EDITOR_CLOSE, @@ -1207,6 +1216,7 @@ interface IEditorPartConfiguration { tabActionLocation?: 'left' | 'right'; tabActionCloseVisibility?: boolean; tabActionUnpinVisibility?: boolean; + alwaysShowEditorActions?: boolean; tabSizing?: 'fit' | 'shrink' | 'fixed'; tabSizingFixedMinWidth?: number; tabSizingFixedMaxWidth?: number; diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 60047f630e3..6f04a87f062 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -25,6 +25,7 @@ export interface IEditorOpenOptions { readonly sticky?: boolean; readonly transient?: boolean; active?: boolean; + readonly inactiveSelection?: EditorInput[]; readonly index?: number; readonly supportSideBySide?: SideBySideEditor.ANY | SideBySideEditor.BOTH; } @@ -174,6 +175,7 @@ export interface IReadonlyEditorGroupModel { readonly isLocked: boolean; readonly activeEditor: EditorInput | null; readonly previewEditor: EditorInput | null; + readonly selectedEditors: EditorInput[]; getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[]; getEditorByIndex(index: number): EditorInput | undefined; @@ -181,6 +183,7 @@ export interface IReadonlyEditorGroupModel { isActive(editor: EditorInput | IUntypedEditorInput): boolean; isPinned(editorOrIndex: EditorInput | number): boolean; isSticky(editorOrIndex: EditorInput | number): boolean; + isSelected(editorOrIndex: EditorInput | number): boolean; isTransient(editorOrIndex: EditorInput | number): boolean; isFirst(editor: EditorInput, editors?: EditorInput[]): boolean; isLast(editor: EditorInput, editors?: EditorInput[]): boolean; @@ -193,6 +196,7 @@ interface IEditorGroupModel extends IReadonlyEditorGroupModel { closeEditor(editor: EditorInput, context?: EditorCloseContext, openNext?: boolean): IEditorCloseResult | undefined; moveEditor(editor: EditorInput, toIndex: number): EditorInput | undefined; setActive(editor: EditorInput | undefined): EditorInput | undefined; + setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): void; } export class EditorGroupModel extends Disposable implements IEditorGroupModel { @@ -201,7 +205,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { //#region events - private readonly _onDidModelChange = this._register(new Emitter()); + private readonly _onDidModelChange = this._register(new Emitter({ leakWarningThreshold: 500 /* increased for users with hundreds of inputs opened */ })); readonly onDidModelChange = this._onDidModelChange.event; //#endregion @@ -216,10 +220,15 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { private locked = false; - private preview: EditorInput | null = null; // editor in preview state - private active: EditorInput | null = null; // editor in active state - private sticky = -1; // index of first editor in sticky state - private transient = new Set(); // editors in transient state + private selection: EditorInput[] = []; // editors in selected state, first one is active + + private get active(): EditorInput | null { + return this.selection[0] ?? null; + } + + private preview: EditorInput | null = null; // editor in preview state + private sticky = -1; // index of first editor in sticky state + private readonly transient = new Set(); // editors in transient state private editorOpenPositioning: ('left' | 'right' | 'first' | 'last') | undefined; private focusRecentEditorAfterClose: boolean | undefined; @@ -287,8 +296,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { return this.active; } - isActive(editor: EditorInput | IUntypedEditorInput): boolean { - return this.matches(this.active, editor); + isActive(candidate: EditorInput | IUntypedEditorInput): boolean { + return this.matches(this.active, candidate); } get previewEditor(): EditorInput | null { @@ -299,7 +308,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const makeSticky = options?.sticky || (typeof options?.index === 'number' && this.isSticky(options.index)); const makePinned = options?.pinned || options?.sticky; const makeTransient = !!options?.transient; - const makeActive = options?.active || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor)); + const makeActive = options?.active || !this.activeEditor || (!makePinned && this.preview === this.activeEditor); const existingEditorAndIndex = this.findEditor(candidate, options); @@ -401,10 +410,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { }; this._onDidModelChange.fire(event); - // Handle active - if (makeActive) { - this.doSetActive(newEditor, targetIndex); - } + // Handle active editor / selected editors + this.setSelection(makeActive ? newEditor : this.activeEditor, options?.inactiveSelection ?? []); return { editor: newEditor, @@ -424,10 +431,8 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.doPin(existingEditor, existingEditorIndex); } - // Activate it - if (makeActive) { - this.doSetActive(existingEditor, existingEditorIndex); - } + // Handle active editor / selected editors + this.setSelection(makeActive ? existingEditor : this.activeEditor, options?.inactiveSelection ?? []); // Respect index if (options && typeof options.index === 'number') { @@ -545,8 +550,9 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const editor = this.editors[index]; const sticky = this.isSticky(index); - // Active Editor closed - if (openNext && this.matches(this.active, editor)) { + // Active editor closed + const isActiveEditor = this.active === editor; + if (openNext && isActiveEditor) { // More than one editor if (this.mru.length > 1) { @@ -561,17 +567,29 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { } } - this.doSetActive(newActive, this.editors.indexOf(newActive)); + // Select editor as active + const newInactiveSelectedEditors = this.selection.filter(selected => selected !== editor && selected !== newActive); + this.doSetSelection(newActive, this.editors.indexOf(newActive), newInactiveSelectedEditors); } - // One Editor + // Last editor closed: clear selection else { - this.active = null; + this.doSetSelection(null, undefined, []); + } + } + + // Inactive editor closed + else if (!isActiveEditor) { + + // Remove editor from inactive selection + if (this.doIsSelected(editor)) { + const newInactiveSelectedEditors = this.selection.filter(selected => selected !== editor && selected !== this.activeEditor); + this.doSetSelection(this.activeEditor, this.indexOf(this.activeEditor), newInactiveSelectedEditors); } } // Preview Editor closed - if (this.matches(this.preview, editor)) { + if (this.preview === editor) { this.preview = null; } @@ -666,30 +684,99 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { const [editor, editorIndex] = res; - this.doSetActive(editor, editorIndex); + this.doSetSelection(editor, editorIndex, []); return editor; } - private doSetActive(editor: EditorInput, editorIndex: number): void { - if (this.matches(this.active, editor)) { - return; // already active + get selectedEditors(): EditorInput[] { + return this.editors.filter(editor => this.doIsSelected(editor)); // return in sequential order + } + + isSelected(editorCandidateOrIndex: EditorInput | number): boolean { + let editor: EditorInput | undefined; + if (typeof editorCandidateOrIndex === 'number') { + editor = this.editors[editorCandidateOrIndex]; + } else { + editor = this.findEditor(editorCandidateOrIndex)?.[0]; } - this.active = editor; + return !!editor && this.doIsSelected(editor); + } - // Bring to front in MRU list - const mruIndex = this.indexOf(editor, this.mru); - this.mru.splice(mruIndex, 1); - this.mru.unshift(editor); + private doIsSelected(editor: EditorInput): boolean { + return this.selection.includes(editor); + } - // Event - const event: IGroupEditorChangeEvent = { - kind: GroupModelChangeKind.EDITOR_ACTIVE, - editor, - editorIndex - }; - this._onDidModelChange.fire(event); + setSelection(activeSelectedEditorCandidate: EditorInput, inactiveSelectedEditorCandidates: EditorInput[]): void { + const res = this.findEditor(activeSelectedEditorCandidate); + if (!res) { + return; // not found + } + + const [activeSelectedEditor, activeSelectedEditorIndex] = res; + + const inactiveSelectedEditors = new Set(); + for (const inactiveSelectedEditorCandidate of inactiveSelectedEditorCandidates) { + const res = this.findEditor(inactiveSelectedEditorCandidate); + if (!res) { + return; // not found + } + + const [inactiveSelectedEditor] = res; + if (inactiveSelectedEditor === activeSelectedEditor) { + continue; // already selected + } + + inactiveSelectedEditors.add(inactiveSelectedEditor); + } + + this.doSetSelection(activeSelectedEditor, activeSelectedEditorIndex, Array.from(inactiveSelectedEditors)); + } + + private doSetSelection(activeSelectedEditor: EditorInput | null, activeSelectedEditorIndex: number | undefined, inactiveSelectedEditors: EditorInput[]): void { + const previousActiveEditor = this.activeEditor; + const previousSelection = this.selection; + + let newSelection: EditorInput[]; + if (activeSelectedEditor) { + newSelection = [activeSelectedEditor, ...inactiveSelectedEditors]; + } else { + newSelection = []; + } + + // Update selection + this.selection = newSelection; + + // Update active editor if it has changed + const activeEditorChanged = activeSelectedEditor && typeof activeSelectedEditorIndex === 'number' && previousActiveEditor !== activeSelectedEditor; + if (activeEditorChanged) { + + // Bring to front in MRU list + const mruIndex = this.indexOf(activeSelectedEditor, this.mru); + this.mru.splice(mruIndex, 1); + this.mru.unshift(activeSelectedEditor); + + // Event + const event: IGroupEditorChangeEvent = { + kind: GroupModelChangeKind.EDITOR_ACTIVE, + editor: activeSelectedEditor, + editorIndex: activeSelectedEditorIndex + }; + this._onDidModelChange.fire(event); + } + + // Fire event if the selection has changed + if ( + activeEditorChanged || + previousSelection.length !== newSelection.length || + previousSelection.some(editor => !newSelection.includes(editor)) + ) { + const event: IGroupModelChangeEvent = { + kind: GroupModelChangeKind.EDITORS_SELECTION + }; + this._onDidModelChange.fire(event); + } } setIndex(index: number) { @@ -777,12 +864,12 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { } } - isPinned(editorOrIndex: EditorInput | number): boolean { + isPinned(editorCandidateOrIndex: EditorInput | number): boolean { let editor: EditorInput; - if (typeof editorOrIndex === 'number') { - editor = this.editors[editorOrIndex]; + if (typeof editorCandidateOrIndex === 'number') { + editor = this.editors[editorCandidateOrIndex]; } else { - editor = editorOrIndex; + editor = editorCandidateOrIndex; } return !this.matches(this.preview, editor); @@ -919,16 +1006,16 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this._onDidModelChange.fire(event); } - isTransient(editorOrIndex: EditorInput | number): boolean { + isTransient(editorCandidateOrIndex: EditorInput | number): boolean { if (this.transient.size === 0) { return false; // no transient editor } let editor: EditorInput | undefined; - if (typeof editorOrIndex === 'number') { - editor = this.editors[editorOrIndex]; + if (typeof editorCandidateOrIndex === 'number') { + editor = this.editors[editorCandidateOrIndex]; } else { - editor = this.findEditor(editorOrIndex)?.[0]; + editor = this.findEditor(editorCandidateOrIndex)?.[0]; } return !!editor && this.transient.has(editor); @@ -1079,7 +1166,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { clone.editors = this.editors.slice(0); clone.mru = this.mru.slice(0); clone.preview = this.preview; - clone.active = this.active; + clone.selection = this.selection.slice(0); clone.sticky = this.sticky; // Ensure to register listeners for each editor @@ -1181,7 +1268,7 @@ export class EditorGroupModel extends Disposable implements IEditorGroupModel { this.mru = coalesce(data.mru.map(i => this.editors[i])); - this.active = this.mru[0]; + this.selection = this.mru.length > 0 ? [this.mru[0]] : []; if (typeof data.preview === 'number') { this.preview = this.editors[data.preview]; diff --git a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts index 390b19874c8..61d4f6a7c80 100644 --- a/src/vs/workbench/common/editor/filteredEditorGroupModel.ts +++ b/src/vs/workbench/common/editor/filteredEditorGroupModel.ts @@ -36,11 +36,13 @@ abstract class FilteredEditorGroupModel extends Disposable implements IReadonlyE get activeEditor(): EditorInput | null { return this.model.activeEditor && this.filter(this.model.activeEditor) ? this.model.activeEditor : null; } get previewEditor(): EditorInput | null { return this.model.previewEditor && this.filter(this.model.previewEditor) ? this.model.previewEditor : null; } + get selectedEditors(): EditorInput[] { return this.model.selectedEditors.filter(e => this.filter(e)); } isPinned(editorOrIndex: EditorInput | number): boolean { return this.model.isPinned(editorOrIndex); } isTransient(editorOrIndex: EditorInput | number): boolean { return this.model.isTransient(editorOrIndex); } isSticky(editorOrIndex: EditorInput | number): boolean { return this.model.isSticky(editorOrIndex); } isActive(editor: EditorInput | IUntypedEditorInput): boolean { return this.model.isActive(editor); } + isSelected(editorOrIndex: EditorInput | number): boolean { return this.model.isSelected(editorOrIndex); } isFirst(editor: EditorInput): boolean { return this.model.isFirst(editor, this.getEditors(EditorsOrder.SEQUENTIAL)); diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index b6a90af3469..3412f81c3bf 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -166,6 +166,28 @@ 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_HOVER_BORDER = registerColor('tab.hoverBorder', { dark: null, light: null, diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index aa516a28537..9a00c6035bf 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -8,16 +8,17 @@ import { DynamicSpeechAccessibilityConfiguration, registerAccessibilityConfigura import { IWorkbenchContributionsRegistry, WorkbenchPhase, Extensions as WorkbenchExtensions, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IAccessibleViewService, AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution'; -import { ExtensionAccessibilityHelpDialogContribution, CommentAccessibleViewContribution, HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus'; import { EditorAccessibilityHelpContribution } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/saveAccessibilitySignal'; -import { CommentsAccessibilityHelpContribution } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; import { DiffEditorActiveAnnouncementContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/openDiffEditorAnnouncement'; import { SpeechAccessibilitySignalContribution } from 'vs/workbench/contrib/speech/browser/speechAccessibilitySignal'; import { AccessibleViewInformationService, IAccessibleViewInformationService } from 'vs/workbench/services/accessibility/common/accessibleViewInformationService'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccesibleViewHelpContribution, AccesibleViewContributions } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; +import { ExtensionAccessibilityHelpDialogContribution } from 'vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution'; registerAccessibilityConfiguration(); registerSingleton(IAccessibleViewService, AccessibleViewService, InstantiationType.Delayed); @@ -25,13 +26,10 @@ registerSingleton(IAccessibleViewInformationService, AccessibleViewInformationSe const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(EditorAccessibilityHelpContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(CommentsAccessibilityHelpContribution, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(UnfocusedViewDimmingContribution, LifecyclePhase.Restored); -workbenchRegistry.registerWorkbenchContribution(HoverAccessibleViewContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(NotificationAccessibleViewContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(CommentAccessibleViewContribution, LifecyclePhase.Eventually); -workbenchRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(AccesibleViewHelpContribution, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(AccesibleViewContributions, LifecyclePhase.Eventually); registerWorkbenchContribution2(AccessibilityStatus.ID, AccessibilityStatus, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ExtensionAccessibilityHelpDialogContribution.ID, ExtensionAccessibilityHelpDialogContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index ef8197fa3b0..453b43eb916 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -9,11 +9,12 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { workbenchConfigurationNodeBase, Extensions as WorkbenchExtensions, IConfigurationMigrationRegistry, ConfigurationKeyValuePairs, ConfigurationMigration } from 'vs/workbench/common/configuration'; import { AccessibilitySignal } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { ISpeechService, SPEECH_LANGUAGES, SPEECH_LANGUAGE_CONFIG } from 'vs/workbench/contrib/speech/common/speechService'; +import { AccessibilityVoiceSettingId, ISpeechService, SPEECH_LANGUAGES } from 'vs/workbench/contrib/speech/common/speechService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Event } from 'vs/base/common/event'; import { isDefined } from 'vs/base/common/types'; +import { IProductService } from 'vs/platform/product/common/productService'; export const accessibilityHelpIsShown = new RawContextKey('accessibilityHelpIsShown', false, true); export const accessibleViewIsShown = new RawContextKey('accessibleViewIsShown', false, true); @@ -59,23 +60,6 @@ export const enum AccessibilityVerbositySettingId { DiffEditorActive = 'accessibility.verbosity.diffEditorActive' } -export const enum AccessibleViewProviderId { - Terminal = 'terminal', - TerminalChat = 'terminal-chat', - TerminalHelp = 'terminal-help', - DiffEditor = 'diffEditor', - Chat = 'panelChat', - InlineChat = 'inlineChat', - InlineCompletions = 'inlineCompletions', - KeybindingsEditor = 'keybindingsEditor', - Notebook = 'notebook', - Editor = 'editor', - Hover = 'hover', - Notification = 'notification', - EmptyEditorHint = 'emptyEditorHint', - Comments = 'comments' -} - const baseVerbosityProperty: IConfigurationPropertySchema = { type: 'boolean', default: true, @@ -203,10 +187,78 @@ const configuration: IConfigurationNode = { 'type': 'boolean', 'default': false, }, + 'delays': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'general': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.general.announcement', "The delay in milliseconds before an announcement is made."), + 'type': 'number', + 'minimum': 0, + }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.general.sound', "The delay in milliseconds before a sound is played."), + 'type': 'number', + 'minimum': 0, + } + }, + }, + 'warningAtPosition': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.warningAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's a warning at the position."), + 'type': 'number', + 'minimum': 0, + }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.warningAtPosition.sound', "The delay in milliseconds before a sound is played when there's a warning at the position."), + 'type': 'number', + 'minimum': 0, + } + }, + }, + 'errorAtPosition': { + 'type': 'object', + 'additionalProperties': false, + 'properties': { + 'announcement': { + 'description': localize('accessibility.signalOptions.delays.errorAtPosition.announcement', "The delay in milliseconds before an announcement is made when there's an error at the position."), + 'type': 'number', + 'minimum': 0, + }, + 'sound': { + 'description': localize('accessibility.signalOptions.delays.errorAtPosition.sound', "The delay in milliseconds before a sound is played when there's an error at the position."), + 'type': 'number', + 'minimum': 0, + } + }, + }, + } + } }, default: { 'volume': 70, - 'debouncePositionChanges': false + 'debouncePositionChanges': false, + 'delays': { + 'general': { + 'announcement': 3000, + 'sound': 400 + }, + 'warningAtPosition': { + 'announcement': 3000, + 'sound': 1000 + }, + 'errorAtPosition': { + 'announcement': 3000, + 'sound': 1000 + } + } }, tags: ['accessibility'] }, @@ -380,6 +432,20 @@ const configuration: IConfigurationNode = { }, } }, + 'accessibility.signals.terminalCommandSucceeded': { + ...signalFeatureBase, + 'description': localize('accessibility.signals.terminalCommandSucceeded', "Plays a signal - sound (audio cue) and/or announcement (alert) - when a terminal command succeeds (zero exit code) or when a command with such an exit code is navigated to in the accessible view."), + 'properties': { + 'sound': { + 'description': localize('accessibility.signals.terminalCommandSucceeded.sound', "Plays a sound when a terminal command succeeds (zero exit code) or when a command with such an exit code is navigated to in the accessible view."), + ...soundFeatureBase + }, + 'announcement': { + 'description': localize('accessibility.signals.terminalCommandSucceeded.announcement', "Announces when a terminal command succeeds (zero exit code) or when a command with such an exit code is navigated to in the accessible view."), + ...announcementFeatureBase + }, + } + }, 'accessibility.signals.terminalQuickFix': { ...signalFeatureBase, 'description': localize('accessibility.signals.terminalQuickFix', "Plays a signal - sound (audio cue) and/or announcement (alert) - when terminal Quick Fixes are available."), @@ -646,10 +712,8 @@ export function registerAccessibilityConfiguration() { }); } -export const enum AccessibilityVoiceSettingId { - SpeechTimeout = 'accessibility.voice.speechTimeout', - SpeechLanguage = SPEECH_LANGUAGE_CONFIG -} +export { AccessibilityVoiceSettingId } + export const SpeechTimeoutDefault = 1200; export class DynamicSpeechAccessibilityConfiguration extends Disposable implements IWorkbenchContribution { @@ -657,7 +721,8 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen static readonly ID = 'workbench.contrib.dynamicSpeechAccessibilityConfiguration'; constructor( - @ISpeechService private readonly speechService: ISpeechService + @ISpeechService private readonly speechService: ISpeechService, + @IProductService private readonly productService: IProductService ) { super(); @@ -686,13 +751,19 @@ export class DynamicSpeechAccessibilityConfiguration extends Disposable implemen 'tags': ['accessibility'] }, [AccessibilityVoiceSettingId.SpeechLanguage]: { - 'markdownDescription': localize('voice.speechLanguage', "The language that voice speech recognition should recognize. Select `auto` to use the configured display language if possible. Note that not all display languages maybe supported by speech recognition"), + 'markdownDescription': localize('voice.speechLanguage', "The language that text-to-speech and speech-to-text should use. Select `auto` to use the configured display language if possible. Note that not all display languages maybe supported by speech recognition and synthesizers."), 'type': 'string', 'enum': languagesSorted, 'default': 'auto', 'tags': ['accessibility'], 'enumDescriptions': languagesSorted.map(key => languages[key].name), 'enumItemLabels': languagesSorted.map(key => languages[key].name) + }, + [AccessibilityVoiceSettingId.AutoSynthesize]: { + 'type': 'boolean', + 'markdownDescription': localize('autoSynthesize', "Whether a textual response should automatically be read out aloud when speech was used as input. For example in a chat session, a response is automatically synthesized when voice was used as chat request."), + 'default': this.productService.quality !== 'stable', // TODO@bpasero decide on a default + 'tags': ['accessibility'] } } }); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index ffd923df3b3..d1a40cc9446 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -9,7 +9,6 @@ import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; @@ -25,6 +24,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; import { localize } from 'vs/nls'; +import { AccessibleViewProviderId, AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol } from 'vs/platform/accessibility/browser/accessibleView'; import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; @@ -33,15 +33,15 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewDelegate, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { resolveContentAndKeybindingItems } from 'vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { IChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/chat'; import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; @@ -51,122 +51,7 @@ const enum DIMENSIONS { MAX_WIDTH = 600 } -type ContentProvider = AdvancedContentProvider | ExtensionContentProvider; - -export class AdvancedContentProvider implements IAccessibleViewContentProvider { - - constructor( - public id: AccessibleViewProviderId, - public options: IAccessibleViewOptions, - public provideContent: () => string, - public onClose: () => void, - public verbositySettingKey: AccessibilityVerbositySettingId, - public actions?: IAction[], - public next?: () => void, - public previous?: () => void, - public onKeyDown?: (e: IKeyboardEvent) => void, - public getSymbols?: () => IAccessibleViewSymbol[], - public onDidRequestClearLastProvider?: Event, - ) { } -} - -export class ExtensionContentProvider implements IBasicContentProvider { - - constructor( - public readonly id: string, - public options: IAccessibleViewOptions, - public provideContent: () => string, - public onClose: () => void, - public next?: () => void, - public previous?: () => void, - public actions?: IAction[], - ) { } -} - -export interface IBasicContentProvider { - id: string; - options: IAccessibleViewOptions; - onClose(): void; - provideContent(): string; - actions?: IAction[]; - previous?(): void; - next?(): void; -} - -export interface IAccessibleViewContentProvider extends IBasicContentProvider { - id: AccessibleViewProviderId; - verbositySettingKey: AccessibilityVerbositySettingId; - /** - * Note that a Codicon class should be provided for each action. - * If not, a default will be used. - */ - onKeyDown?(e: IKeyboardEvent): void; - /** - * When the language is markdown, this is provided by default. - */ - getSymbols?(): IAccessibleViewSymbol[]; - /** - * Note that this will only take effect if the provider has an ID. - */ - onDidRequestClearLastProvider?: Event; -} - -export const IAccessibleViewService = createDecorator('accessibleViewService'); - -export interface IAccessibleViewService { - readonly _serviceBrand: undefined; - show(provider: ContentProvider, position?: Position): void; - showLastProvider(id: AccessibleViewProviderId): void; - showAccessibleViewHelp(): void; - next(): void; - previous(): void; - navigateToCodeBlock(type: 'next' | 'previous'): void; - goToSymbol(): void; - disableHint(): void; - getPosition(id: AccessibleViewProviderId): Position | undefined; - setPosition(position: Position, reveal?: boolean): void; - getLastPosition(): Position | undefined; - /** - * If the setting is enabled, provides the open accessible view hint as a localized string. - * @param verbositySettingKey The setting key for the verbosity of the feature - */ - getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null; - getCodeBlockContext(): ICodeBlockActionContext | undefined; -} - -export const enum AccessibleViewType { - Help = 'help', - View = 'view' -} - -export const enum NavigationType { - Previous = 'previous', - Next = 'next' -} - -export interface IAccessibleViewOptions { - readMoreUrl?: string; - /** - * Defaults to markdown - */ - language?: string; - type: AccessibleViewType; - /** - * By default, places the cursor on the top line of the accessible view. - * If set to 'initial-bottom', places the cursor on the bottom line of the accessible view and preserves it henceforth. - * If set to 'bottom', places the cursor on the bottom line of the accessible view. - */ - position?: 'bottom' | 'initial-bottom'; - /** - * @returns a string that will be used as the content of the help dialog - * instead of the one provided by default. - */ - customHelp?: () => string; - /** - * If this provider might want to request to be shown again, provide an ID. - */ - id?: AccessibleViewProviderId; -} +export type AccesibleViewContentProvider = AdvancedContentProvider | ExtensionContentProvider; interface ICodeBlock { startLine: number; @@ -194,10 +79,10 @@ export class AccessibleView extends Disposable { private _title: HTMLElement; private readonly _toolbar: WorkbenchToolBar; - private _currentProvider: ContentProvider | undefined; + private _currentProvider: AccesibleViewContentProvider | undefined; private _currentContent: string | undefined; - private _lastProvider: ContentProvider | undefined; + private _lastProvider: AccesibleViewContentProvider | undefined; constructor( @IOpenerService private readonly _openerService: IOpenerService, @@ -212,7 +97,8 @@ export class AccessibleView extends Disposable { @IMenuService private readonly _menuService: IMenuService, @ICommandService private readonly _commandService: ICommandService, @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService, - @IStorageService private readonly _storageService: IStorageService + @IStorageService private readonly _storageService: IStorageService, + @IQuickInputService private readonly _quickInputService: IQuickInputService ) { super(); @@ -354,7 +240,7 @@ export class AccessibleView extends Disposable { this.show(this._lastProvider); } - show(provider?: ContentProvider, symbol?: IAccessibleViewSymbol, showAccessibleViewHelp?: boolean, position?: Position): void { + show(provider?: AccesibleViewContentProvider, symbol?: IAccessibleViewSymbol, showAccessibleViewHelp?: boolean, position?: Position): void { provider = provider ?? this._currentProvider; if (!provider) { return; @@ -446,7 +332,7 @@ export class AccessibleView extends Disposable { inBlock = true; startLine = i + 1; languageId = line.substring(3).trim(); - } else if (inBlock && line.startsWith('```')) { + } else if (inBlock && line.endsWith('```')) { inBlock = false; const endLine = i; const code = lines.slice(startLine, endLine).join('\n'); @@ -477,6 +363,40 @@ export class AccessibleView extends Disposable { return symbols.length ? symbols : undefined; } + openHelpLink(): void { + if (!this._currentProvider?.options.readMoreUrl) { + return; + } + this._openerService.open(URI.parse(this._currentProvider.options.readMoreUrl)); + } + + configureKeybindings(): void { + const items = this._currentProvider?.options?.configureKeybindingItems; + const provider = this._currentProvider; + if (!items) { + return; + } + const quickPick: IQuickPick = this._quickInputService.createQuickPick(); + this._register(quickPick); + quickPick.items = items; + quickPick.title = localize('keybindings', 'Configure keybindings'); + quickPick.placeholder = localize('selectKeybinding', 'Select a command ID to configure a keybinding for it'); + quickPick.show(); + quickPick.onDidAccept(async () => { + const item = quickPick.selectedItems[0]; + if (item) { + await this._commandService.executeCommand('workbench.action.openGlobalKeybindings', item.id); + } + quickPick.dispose(); + }); + quickPick.onDidHide(() => { + if (!quickPick.selectedItems.length && provider) { + this.show(provider); + } + quickPick.dispose(); + }); + } + private _convertTokensToSymbols(tokens: marked.TokensList, symbols: IAccessibleViewSymbol[]): void { let firstListItem: string | undefined; for (const token of tokens) { @@ -506,7 +426,7 @@ export class AccessibleView extends Disposable { } } - showSymbol(provider: ContentProvider, symbol: IAccessibleViewSymbol): void { + showSymbol(provider: AccesibleViewContentProvider, symbol: IAccessibleViewSymbol): void { if (!this._currentContent) { return; } @@ -540,7 +460,7 @@ export class AccessibleView extends Disposable { alert(localize('disableAccessibilityHelp', '{0} accessibility verbosity is now disabled', this._currentProvider.verbositySettingKey)); } - private _updateContextKeys(provider: ContentProvider, shown: boolean): void { + private _updateContextKeys(provider: AccesibleViewContentProvider, shown: boolean): void { if (provider.options.type === AccessibleViewType.Help) { this._accessiblityHelpIsShown.set(shown); this._accessibleViewIsShown.reset(); @@ -553,14 +473,14 @@ export class AccessibleView extends Disposable { this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false); } - private _render(provider: ContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable { + private _render(provider: AccesibleViewContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean): IDisposable { this._currentProvider = provider; this._accessibleViewCurrentProviderId.set(provider.id); const verbose = this._verbosityEnabled(); - const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility (H).") : ''; + const readMoreLink = provider.options.readMoreUrl ? localize("openDoc", "\n\nOpen a browser window with more information related to accessibility.", AccessibilityCommandId.AccessibilityHelpOpenHelpLink) : ''; let disableHelpHint = ''; if (provider instanceof AdvancedContentProvider && provider.options.type === AccessibleViewType.Help && verbose) { - disableHelpHint = this._getDisableVerbosityHint(provider.verbositySettingKey); + disableHelpHint = this._getDisableVerbosityHint(); } const accessibilitySupport = this._accessibilityService.isScreenReaderOptimized(); let message = ''; @@ -579,7 +499,17 @@ export class AccessibleView extends Disposable { } } const exitThisDialogHint = verbose && !provider.options.position ? localize('exit', '\n\nExit this dialog (Escape).') : ''; - const newContent = message + provider.provideContent() + readMoreLink + disableHelpHint + exitThisDialogHint; + let content = provider.provideContent(); + if (provider.options.type === AccessibleViewType.Help) { + const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, content + readMoreLink + disableHelpHint + exitThisDialogHint); + if (resolvedContent) { + content = resolvedContent.content.value; + if (resolvedContent.configureKeybindingItems) { + provider.options.configureKeybindingItems = resolvedContent.configureKeybindingItems; + } + } + } + const newContent = message + content; this.calculateCodeBlocks(newContent); this._currentContent = newContent; this._updateContextKeys(provider, true); @@ -709,7 +639,7 @@ export class AccessibleView extends Disposable { return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || (this._currentProvider instanceof AdvancedContentProvider && !!this._currentProvider.getSymbols?.()); } - private _updateLastProvider(): ContentProvider | undefined { + private _updateLastProvider(): AccesibleViewContentProvider | undefined { const provider = this._currentProvider; if (!provider) { return; @@ -785,66 +715,24 @@ export class AccessibleView extends Disposable { if (this._currentProvider?.id !== AccessibleViewProviderId.Chat) { return; } - let hint = ''; - const insertAtCursorKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertCodeBlock')?.getAriaLabel(); - const insertIntoNewFileKb = this._keybindingService.lookupKeybinding('workbench.action.chat.insertIntoNewFile')?.getAriaLabel(); - const runInTerminalKb = this._keybindingService.lookupKeybinding('workbench.action.chat.runInTerminal')?.getAriaLabel(); - - if (insertAtCursorKb) { - hint += localize('insertAtCursor', " - Insert the code block at the cursor ({0}).\n", insertAtCursorKb); - } else { - hint += localize('insertAtCursorNoKb', " - Insert the code block at the cursor by configuring a keybinding for the Chat: Insert Code Block command.\n"); - } - if (insertIntoNewFileKb) { - hint += localize('insertIntoNewFile', " - Insert the code block into a new file ({0}).\n", insertIntoNewFileKb); - } else { - hint += localize('insertIntoNewFileNoKb', " - Insert the code block into a new file by configuring a keybinding for the Chat: Insert into New File command.\n"); - } - if (runInTerminalKb) { - hint += localize('runInTerminal', " - Run the code block in the terminal ({0}).\n", runInTerminalKb); - } else { - hint += localize('runInTerminalNoKb', " - Run the coe block in the terminal by configuring a keybinding for the Chat: Insert into Terminal command.\n"); - } - - return hint; + return [localize('insertAtCursor', " - Insert the code block at the cursor."), + localize('insertIntoNewFile', " - Insert the code block into a new file."), + localize('runInTerminal', " - Run the code block in the terminal.\n")].join('\n'); } private _getNavigationHint(): string { - let hint = ''; - const nextKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowNext)?.getAriaLabel(); - const previousKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.ShowPrevious)?.getAriaLabel(); - if (nextKeybinding && previousKeybinding) { - hint = localize('accessibleViewNextPreviousHint', "Show the next ({0}) or previous ({1}) item.", nextKeybinding, previousKeybinding); - } else { - hint = localize('chatAccessibleViewNextPreviousHintNoKb', "Show the next or previous item by configuring keybindings for the Show Next & Previous in Accessible View commands."); - } - return hint; - } - private _getDisableVerbosityHint(verbositySettingKey: AccessibilityVerbositySettingId): string { - if (!this._configurationService.getValue(verbositySettingKey)) { - return ''; - } - let hint = ''; - const disableKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.DisableVerbosityHint, this._contextKeyService)?.getAriaLabel(); - if (disableKeybinding) { - hint = localize('acessibleViewDisableHint', "\n\nDisable accessibility verbosity for this feature ({0}).", disableKeybinding); - } else { - hint = localize('accessibleViewDisableHintNoKb', "\n\nAdd a keybinding for the command Disable Accessible View Hint, which disables accessibility verbosity for this feature.s"); - } - return hint; + return localize('accessibleViewNextPreviousHint', "Show the next item or previous item.", AccessibilityCommandId.ShowNext, AccessibilityCommandId.ShowPrevious); } - private _getGoToSymbolHint(providerHasSymbols?: boolean): string { - const goToSymbolKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.GoToSymbol)?.getAriaLabel(); - let goToSymbolHint = ''; - if (providerHasSymbols) { - if (goToSymbolKb) { - goToSymbolHint = localize('goToSymbolHint', 'Go to a symbol ({0}).', goToSymbolKb); - } else { - goToSymbolHint = localize('goToSymbolHintNoKb', 'To go to a symbol, configure a keybinding for the command Go To Symbol in Accessible View'); - } + private _getDisableVerbosityHint(): string { + return localize('acessibleViewDisableHint', "\n\nDisable accessibility verbosity for this feature.", AccessibilityCommandId.DisableVerbosityHint); + } + + private _getGoToSymbolHint(providerHasSymbols?: boolean): string | undefined { + if (!providerHasSymbols) { + return; } - return goToSymbolHint; + return localize('goToSymbolHint', 'Go to a symbol.', AccessibilityCommandId.GoToSymbol); } } @@ -860,12 +748,18 @@ export class AccessibleViewService extends Disposable implements IAccessibleView super(); } - show(provider: ContentProvider, position?: Position): void { + show(provider: AccesibleViewContentProvider, position?: Position): void { if (!this._accessibleView) { this._accessibleView = this._register(this._instantiationService.createInstance(AccessibleView)); } this._accessibleView.show(provider, undefined, undefined, position); } + configureKeybindings(): void { + this._accessibleView?.configureKeybindings(); + } + openHelpLink(): void { + this._accessibleView?.openHelpLink(); + } showLastProvider(id: AccessibleViewProviderId): void { this._accessibleView?.showLastProvider(id); } @@ -923,7 +817,7 @@ class AccessibleViewSymbolQuickPick { constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) { } - show(provider: ContentProvider): void { + show(provider: AccesibleViewContentProvider): void { const quickPick = this._quickInputService.createQuickPick(); quickPick.placeholder = localize('accessibleViewSymbolQuickPickPlaceholder', "Type to search symbols"); quickPick.title = localize('accessibleViewSymbolQuickPickTitle', "Go to Symbol Accessible View"); @@ -954,12 +848,6 @@ class AccessibleViewSymbolQuickPick { } } -export interface IAccessibleViewSymbol extends IPickerQuickAccessItem { - markdownToParse?: string; - firstListItem?: string; - lineNumber?: number; - endLineNumber?: number; -} function shouldHide(event: KeyboardEvent, keybindingService: IKeybindingService, configurationService: IConfigurationService): boolean { if (!configurationService.getValue(AccessibilityWorkbenchSettingId.AccessibleViewCloseOnKeyPress)) { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index d83fb4e7365..d4f6472c0c1 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -11,8 +11,8 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { AccessibleViewProviderId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewIsShown, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; @@ -66,7 +66,7 @@ class AccessibleViewNextCodeBlockAction extends Action2 { menu: { ...accessibleViewMenu, - when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation), + when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewContainsCodeBlocks), }, title: localize('editor.action.accessibleViewNextCodeBlock', "Accessible View: Next Code Block") }); @@ -91,7 +91,7 @@ class AccessibleViewPreviousCodeBlockAction extends Action2 { icon: Codicon.arrowLeft, menu: { ...accessibleViewMenu, - when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewSupportsNavigation), + when: ContextKeyExpr.and(accessibleViewIsShown, accessibleViewContainsCodeBlocks), }, title: localize('editor.action.accessibleViewPreviousCodeBlock', "Accessible View: Previous Code Block") }); @@ -228,6 +228,43 @@ class AccessibleViewDisableHintAction extends Action2 { } registerAction2(AccessibleViewDisableHintAction); +class AccessibilityHelpConfigureKeybindingsAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.AccessibilityHelpConfigureKeybindings, + precondition: ContextKeyExpr.and(accessibilityHelpIsShown), + keybinding: { + primary: KeyMod.Alt | KeyCode.KeyK, + weight: KeybindingWeight.WorkbenchContrib + }, + title: localize('editor.action.accessibilityHelpConfigureKeybindings', "Accessibility Help Configure Keybindings") + }); + } + async run(accessor: ServicesAccessor): Promise { + await accessor.get(IAccessibleViewService).configureKeybindings(); + } +} +registerAction2(AccessibilityHelpConfigureKeybindingsAction); + + +class AccessibilityHelpOpenHelpLinkAction extends Action2 { + constructor() { + super({ + id: AccessibilityCommandId.AccessibilityHelpOpenHelpLink, + precondition: ContextKeyExpr.and(accessibilityHelpIsShown), + keybinding: { + primary: KeyMod.Alt | KeyCode.KeyH, + weight: KeybindingWeight.WorkbenchContrib + }, + title: localize('editor.action.accessibilityHelpOpenHelpLink', "Accessibility Help Open Help Link") + }); + } + run(accessor: ServicesAccessor): void { + accessor.get(IAccessibleViewService).openHelpLink(); + } +} +registerAction2(AccessibilityHelpOpenHelpLinkAction); + class AccessibleViewAcceptInlineCompletionAction extends Action2 { constructor() { super({ @@ -267,3 +304,4 @@ class AccessibleViewAcceptInlineCompletionAction extends Action2 { } } registerAction2(AccessibleViewAcceptInlineCompletionAction); + diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts index c01a8f828ea..d8d07767246 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewContributions.ts @@ -3,425 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableMap, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { localize } from 'vs/nls'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import * as strings from 'vs/base/common/strings'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { getNotificationFromContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; -import { IListService, WorkbenchList } from 'vs/platform/list/browser/listService'; -import { FocusedViewContext, NotificationFocusedContext } from 'vs/workbench/common/contextkeys'; -import { IAccessibleViewService, IAccessibleViewOptions, AccessibleViewType, ExtensionContentProvider } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { alert } from 'vs/base/browser/ui/aria/aria'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { IAction } from 'vs/base/common/actions'; -import { INotificationViewItem } from 'vs/workbench/common/notifications'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { Codicon } from 'vs/base/common/codicons'; -import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; -import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService'; -import { Extensions, IViewDescriptor, IViewsRegistry } from 'vs/workbench/common/views'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { COMMENTS_VIEW_ID, CommentsMenus } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { CommentsPanel, CONTEXT_KEY_HAS_COMMENTS } from 'vs/workbench/contrib/comments/browser/commentsView'; -import { IMenuService } from 'vs/platform/actions/common/actions'; -import { MarshalledId } from 'vs/base/common/marshallingIds'; -import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { URI } from 'vs/base/common/uri'; +import { AccessibleViewType, AdvancedContentProvider, ExtensionContentProvider, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { - const kb = keybindingService.lookupKeybinding(commandId); - if (kb) { - return strings.format(msg, kb.getAriaLabel()); - } - return strings.format(noKbMsg, commandId); -} - -export class HoverAccessibleViewContribution extends Disposable { - static ID: 'hoverAccessibleViewContribution'; - private _options: IAccessibleViewOptions = { language: 'typescript', type: AccessibleViewType.View }; +export class AccesibleViewHelpContribution extends Disposable { + static ID: 'accesibleViewHelpContribution'; constructor() { super(); - this._register(AccessibleViewAction.addImplementation(95, 'hover', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const codeEditorService = accessor.get(ICodeEditorService); - const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - const editorHoverContent = editor ? HoverController.get(editor)?.getWidgetContent() ?? undefined : undefined; - if (!editor || !editorHoverContent) { - return false; - } - this._options.language = editor?.getModel()?.getLanguageId() ?? undefined; - accessibleViewService.show({ - id: AccessibleViewProviderId.Hover, - verbositySettingKey: AccessibilityVerbositySettingId.Hover, - provideContent() { return editorHoverContent; }, - onClose() { - HoverController.get(editor)?.focus(); - }, - options: this._options - }); - return true; - }, EditorContextKeys.hoverFocused)); - this._register(AccessibleViewAction.addImplementation(90, 'extension-hover', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const contextViewService = accessor.get(IContextViewService); - const contextViewElement = contextViewService.getContextViewElement(); - const extensionHoverContent = contextViewElement?.textContent ?? undefined; - const hoverService = accessor.get(IHoverService); - - if (contextViewElement.classList.contains('accessible-view-container') || !extensionHoverContent) { - // The accessible view, itself, uses the context view service to display the text. We don't want to read that. - return false; - } - accessibleViewService.show({ - id: AccessibleViewProviderId.Hover, - verbositySettingKey: AccessibilityVerbositySettingId.Hover, - provideContent() { return extensionHoverContent; }, - onClose() { - hoverService.showAndFocusLastHover(); - }, - options: this._options - }); - return true; - })); - this._register(AccessibilityHelpAction.addImplementation(115, 'accessible-view', accessor => { + this._register(AccessibilityHelpAction.addImplementation(115, 'accessible-view-help', accessor => { accessor.get(IAccessibleViewService).showAccessibleViewHelp(); return true; }, accessibleViewIsShown)); } } -export class NotificationAccessibleViewContribution extends Disposable { - static ID: 'notificationAccessibleViewContribution'; +export class AccesibleViewContributions extends Disposable { + static ID: 'accesibleViewContributions'; constructor() { super(); - this._register(AccessibleViewAction.addImplementation(90, 'notifications', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const listService = accessor.get(IListService); - const commandService = accessor.get(ICommandService); - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - - function renderAccessibleView(): boolean { - const notification = getNotificationFromContext(listService); - if (!notification) { - return false; + AccessibleViewRegistry.getImplementations().forEach(impl => { + const implementation = (accessor: ServicesAccessor) => { + const provider: AdvancedContentProvider | ExtensionContentProvider | undefined = impl.getProvider(accessor); + if (provider) { + accessor.get(IAccessibleViewService).show(provider); + return true; } - commandService.executeCommand('notifications.showList'); - let notificationIndex: number | undefined; - let length: number | undefined; - const list = listService.lastFocusedList; - if (list instanceof WorkbenchList) { - notificationIndex = list.indexOf(notification); - length = list.length; - } - if (notificationIndex === undefined) { - return false; - } - - function focusList(): void { - commandService.executeCommand('notifications.showList'); - if (list && notificationIndex !== undefined) { - list.domFocus(); - try { - list.setFocus([notificationIndex]); - } catch { } - } - } - const message = notification.message.original.toString(); - if (!message) { - return false; - } - notification.onDidClose(() => accessibleViewService.next()); - accessibleViewService.show({ - id: AccessibleViewProviderId.Notification, - provideContent: () => { - return notification.source ? localize('notification.accessibleViewSrc', '{0} Source: {1}', message, notification.source) : localize('notification.accessibleView', '{0}', message); - }, - onClose(): void { - focusList(); - }, - next(): void { - if (!list) { - return; - } - focusList(); - list.focusNext(); - alertFocusChange(notificationIndex, length, 'next'); - renderAccessibleView(); - }, - previous(): void { - if (!list) { - return; - } - focusList(); - list.focusPrevious(); - alertFocusChange(notificationIndex, length, 'previous'); - renderAccessibleView(); - }, - verbositySettingKey: AccessibilityVerbositySettingId.Notification, - options: { type: AccessibleViewType.View }, - actions: getActionsFromNotification(notification, accessibilitySignalService) - }); - return true; - } - return renderAccessibleView(); - }, NotificationFocusedContext)); - } -} - -function getActionsFromNotification(notification: INotificationViewItem, accessibilitySignalService: IAccessibilitySignalService): IAction[] | undefined { - let actions = undefined; - if (notification.actions) { - actions = []; - if (notification.actions.primary) { - actions.push(...notification.actions.primary); - } - if (notification.actions.secondary) { - actions.push(...notification.actions.secondary); - } - } - if (actions) { - for (const action of actions) { - action.class = ThemeIcon.asClassName(Codicon.bell); - const initialAction = action.run; - action.run = () => { - initialAction(); - notification.close(); + return false; }; - } - } - const manageExtension = actions?.find(a => a.label.includes('Manage Extension')); - if (manageExtension) { - manageExtension.class = ThemeIcon.asClassName(Codicon.gear); - } - if (actions) { - actions.push({ - id: 'clearNotification', label: localize('clearNotification', "Clear Notification"), tooltip: localize('clearNotification', "Clear Notification"), run: () => { - notification.close(); - accessibilitySignalService.playSignal(AccessibilitySignal.clear); - }, enabled: true, class: ThemeIcon.asClassName(Codicon.clearAll) + if (impl.type === AccessibleViewType.View) { + this._register(AccessibleViewAction.addImplementation(impl.priority, impl.name, implementation, impl.when)); + } else { + this._register(AccessibilityHelpAction.addImplementation(impl.priority, impl.name, implementation, impl.when)); + } }); } - return actions; -} - -export function alertFocusChange(index: number | undefined, length: number | undefined, type: 'next' | 'previous'): void { - if (index === undefined || length === undefined) { - return; - } - const number = index + 1; - - if (type === 'next' && number + 1 <= length) { - alert(`Focused ${number + 1} of ${length}`); - } else if (type === 'previous' && number - 1 > 0) { - alert(`Focused ${number - 1} of ${length}`); - } - return; -} - - -export class CommentAccessibleViewContribution extends Disposable { - static ID: 'commentAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(90, 'comment', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const contextKeyService = accessor.get(IContextKeyService); - const viewsService = accessor.get(IViewsService); - const menuService = accessor.get(IMenuService); - const commentsView = viewsService.getActiveViewWithId(COMMENTS_VIEW_ID); - if (!commentsView) { - return false; - } - const menus = this._register(new CommentsMenus(menuService)); - menus.setContextKeyService(contextKeyService); - - function renderAccessibleView() { - if (!commentsView) { - return false; - } - - const commentNode = commentsView.focusedCommentNode; - const content = commentsView.focusedCommentInfo?.toString(); - if (!commentNode || !content) { - return false; - } - const menuActions = [...menus.getResourceContextActions(commentNode)].filter(i => i.enabled); - const actions = menuActions.map(action => { - return { - ...action, - run: () => { - commentsView.focus(); - action.run({ - thread: commentNode.thread, - $mid: MarshalledId.CommentThread, - commentControlHandle: commentNode.controllerHandle, - commentThreadHandle: commentNode.threadHandle, - }); - } - }; - }); - accessibleViewService.show({ - id: AccessibleViewProviderId.Notification, - provideContent: () => { - return content; - }, - onClose(): void { - commentsView.focus(); - }, - next(): void { - commentsView.focus(); - commentsView.focusNextNode(); - renderAccessibleView(); - }, - previous(): void { - commentsView.focus(); - commentsView.focusPreviousNode(); - renderAccessibleView(); - }, - verbositySettingKey: AccessibilityVerbositySettingId.Comments, - options: { type: AccessibleViewType.View }, - actions - }); - return true; - } - return renderAccessibleView(); - }, CONTEXT_KEY_HAS_COMMENTS)); - } -} - -export class InlineCompletionsAccessibleViewContribution extends Disposable { - static ID: 'inlineCompletionsAccessibleViewContribution'; - private _options: IAccessibleViewOptions = { type: AccessibleViewType.View }; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(95, 'inline-completions', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const codeEditorService = accessor.get(ICodeEditorService); - const show = () => { - const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - if (!editor) { - return false; - } - const model = InlineCompletionsController.get(editor)?.model.get(); - const state = model?.state.get(); - if (!model || !state) { - return false; - } - const lineText = model.textModel.getLineContent(state.primaryGhostText.lineNumber); - const ghostText = state.primaryGhostText.renderForScreenReader(lineText); - if (!ghostText) { - return false; - } - this._options.language = editor.getModel()?.getLanguageId() ?? undefined; - accessibleViewService.show({ - id: AccessibleViewProviderId.InlineCompletions, - verbositySettingKey: AccessibilityVerbositySettingId.InlineCompletions, - provideContent() { return lineText + ghostText; }, - onClose() { - model.stop(); - editor.focus(); - }, - next() { - model.next(); - setTimeout(() => show(), 50); - }, - previous() { - model.previous(); - setTimeout(() => show(), 50); - }, - options: this._options - }); - return true; - }; ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible); - return show(); - })); - } -} - -export class ExtensionAccessibilityHelpDialogContribution extends Disposable { - static ID = 'extensionAccessibilityHelpDialogContribution'; - private _viewHelpDialogMap = this._register(new DisposableMap()); - constructor(@IKeybindingService keybindingService: IKeybindingService) { - super(); - this._register(Registry.as(Extensions.ViewsRegistry).onViewsRegistered(e => { - for (const view of e) { - for (const viewDescriptor of view.views) { - if (viewDescriptor.accessibilityHelpContent) { - this._viewHelpDialogMap.set(viewDescriptor.id, registerAccessibilityHelpAction(keybindingService, viewDescriptor)); - } - } - } - })); - this._register(Registry.as(Extensions.ViewsRegistry).onViewsDeregistered(e => { - for (const viewDescriptor of e.views) { - if (viewDescriptor.accessibilityHelpContent) { - this._viewHelpDialogMap.get(viewDescriptor.id)?.dispose(); - } - } - })); - } -} - -function registerAccessibilityHelpAction(keybindingService: IKeybindingService, viewDescriptor: IViewDescriptor): IDisposable { - const disposableStore = new DisposableStore(); - const helpContent = resolveExtensionHelpContent(keybindingService, viewDescriptor.accessibilityHelpContent); - if (!helpContent) { - throw new Error('No help content for view'); - } - disposableStore.add(AccessibilityHelpAction.addImplementation(95, viewDescriptor.id, accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const viewsService = accessor.get(IViewsService); - accessibleViewService.show(new ExtensionContentProvider( - viewDescriptor.id, - { type: AccessibleViewType.Help }, - () => helpContent.value, - () => viewsService.openView(viewDescriptor.id, true) - )); - return true; - }, FocusedViewContext.isEqualTo(viewDescriptor.id))); - disposableStore.add(keybindingService.onDidUpdateKeybindings(() => { - disposableStore.clear(); - disposableStore.add(registerAccessibilityHelpAction(keybindingService, viewDescriptor)); - })); - return disposableStore; -} - -function resolveExtensionHelpContent(keybindingService: IKeybindingService, content?: MarkdownString): MarkdownString | undefined { - if (!content) { - return; - } - let resolvedContent = typeof content === 'string' ? content : content.value; - const matches = resolvedContent.matchAll(/\.*)\>/gm); - for (const match of [...matches]) { - const commandId = match?.groups?.commandId; - if (match?.length && commandId) { - const keybinding = keybindingService.lookupKeybinding(commandId)?.getAriaLabel(); - let kbLabel = keybinding; - if (!kbLabel) { - const args = URI.parse(`command:workbench.action.openGlobalKeybindings?${encodeURIComponent(JSON.stringify(commandId))}`); - kbLabel = ` [Configure a keybinding](${args})`; - } else { - kbLabel = ' (' + keybinding + ')'; - } - resolvedContent = resolvedContent.replace(match[0], kbLabel); - } - } - const result = new MarkdownString(resolvedContent); - result.isTrusted = true; - return result; } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts new file mode 100644 index 00000000000..88bfbd309d4 --- /dev/null +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewKeybindingResolver.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; + +export function resolveContentAndKeybindingItems(keybindingService: IKeybindingService, value?: string): { content: MarkdownString; configureKeybindingItems: IPickerQuickAccessItem[] | undefined } | undefined { + if (!value) { + return; + } + const configureKeybindingItems: IPickerQuickAccessItem[] = []; + const matches = value.matchAll(/\.*)\>/gm); + for (const match of [...matches]) { + const commandId = match?.groups?.commandId; + let kbLabel; + if (match?.length && commandId) { + const keybinding = keybindingService.lookupKeybinding(commandId)?.getAriaLabel(); + if (!keybinding) { + const configureKb = keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel(); + const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Keybindings.'; + kbLabel = `, configure a keybinding ` + keybindingToConfigureQuickPick; + configureKeybindingItems.push({ + label: commandId, + id: commandId + }); + } else { + kbLabel = ' (' + keybinding + ')'; + } + value = value.replace(match[0], kbLabel); + } + } + const content = new MarkdownString(value); + content.isTrusted = true; + return { content, configureKeybindingItems: configureKeybindingItems.length ? configureKeybindingItems : undefined }; +} + diff --git a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts index 541b07a6bce..0e47093d20b 100644 --- a/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp.ts @@ -7,28 +7,24 @@ import { Disposable } from 'vs/base/common/lifecycle'; 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 { AccessibilityHelpNLS } from 'vs/editor/common/standaloneStrings'; -import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibleViewProviderId, AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { descriptionForCommand } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; -import { IAccessibleViewService, IAccessibleViewContentProvider, IAccessibleViewOptions, AccessibleViewType } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { CommentAccessibilityHelpNLS } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; -import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; +import { IAccessibleViewService, IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; export class EditorAccessibilityHelpContribution extends Disposable { static ID: 'editorAccessibilityHelpContribution'; constructor() { super(); - this._register(AccessibilityHelpAction.addImplementation(95, 'editor', async accessor => { + this._register(AccessibilityHelpAction.addImplementation(90, 'editor', async accessor => { const codeEditorService = accessor.get(ICodeEditorService); const accessibleViewService = accessor.get(IAccessibleViewService); const instantiationService = accessor.get(IInstantiationService); @@ -39,7 +35,7 @@ export class EditorAccessibilityHelpContribution extends Disposable { codeEditor = codeEditorService.getActiveCodeEditor()!; } accessibleViewService.show(instantiationService.createInstance(EditorAccessibilityHelpProvider, codeEditor)); - }, EditorContextKeys.focus)); + })); } } @@ -89,13 +85,13 @@ class EditorAccessibilityHelpProvider implements IAccessibleViewContentProvider } if (options.get(EditorOption.stickyScroll).enabled) { - content.push(descriptionForCommand('editor.action.focusStickyScroll', AccessibilityHelpNLS.stickScrollKb, AccessibilityHelpNLS.stickScrollNoKb, this._keybindingService)); + content.push(AccessibilityHelpNLS.stickScroll); } if (options.get(EditorOption.tabFocusMode)) { - content.push(descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOnMsg, AccessibilityHelpNLS.tabFocusModeOnMsgNoKb, this._keybindingService)); + content.push(AccessibilityHelpNLS.tabFocusModeOnMsg); } else { - content.push(descriptionForCommand(ToggleTabFocusModeAction.ID, AccessibilityHelpNLS.tabFocusModeOffMsg, AccessibilityHelpNLS.tabFocusModeOffMsgNoKb, this._keybindingService)); + content.push(AccessibilityHelpNLS.tabFocusModeOffMsg); } return content.join('\n\n'); } @@ -104,24 +100,14 @@ class EditorAccessibilityHelpProvider implements IAccessibleViewContentProvider export function getCommentCommandInfo(keybindingService: IKeybindingService, contextKeyService: IContextKeyService, editor: ICodeEditor): string | undefined { const editorContext = contextKeyService.getContext(editor.getDomNode()!); if (editorContext.getValue(CommentContextKeys.activeEditorHasCommentingRange.key)) { - const commentCommandInfo: string[] = []; - commentCommandInfo.push(CommentAccessibilityHelpNLS.intro); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.NextThread, CommentAccessibilityHelpNLS.nextCommentThreadKb, CommentAccessibilityHelpNLS.nextCommentThreadNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.PreviousThread, CommentAccessibilityHelpNLS.previousCommentThreadKb, CommentAccessibilityHelpNLS.previousCommentThreadNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb, keybindingService)); - return commentCommandInfo.join('\n'); + return [CommentAccessibilityHelpNLS.intro, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.nextCommentThread, CommentAccessibilityHelpNLS.previousCommentThread, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.previousRange].join('\n'); } return; } export function getChatCommandInfo(keybindingService: IKeybindingService, contextKeyService: IContextKeyService): string | undefined { if (CONTEXT_CHAT_ENABLED.getValue(contextKeyService)) { - const commentCommandInfo: string[] = []; - commentCommandInfo.push(descriptionForCommand('workbench.action.quickchat.toggle', AccessibilityHelpNLS.quickChat, AccessibilityHelpNLS.quickChatNoKb, keybindingService)); - commentCommandInfo.push(descriptionForCommand('inlineChat.start', AccessibilityHelpNLS.startInlineChat, AccessibilityHelpNLS.startInlineChatNoKb, keybindingService)); - return commentCommandInfo.join('\n'); + return [AccessibilityHelpNLS.quickChat, AccessibilityHelpNLS.startInlineChat].join('\n'); } return; } diff --git a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts new file mode 100644 index 00000000000..46ea96cf1f0 --- /dev/null +++ b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableMap, IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { AccessibleViewType, ExtensionContentProvider } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { FocusedViewContext } from 'vs/workbench/common/contextkeys'; +import { IViewsRegistry, Extensions, IViewDescriptor } from 'vs/workbench/common/views'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; + +export class ExtensionAccessibilityHelpDialogContribution extends Disposable { + static ID = 'extensionAccessibilityHelpDialogContribution'; + private _viewHelpDialogMap = this._register(new DisposableMap()); + constructor(@IKeybindingService keybindingService: IKeybindingService) { + super(); + this._register(Registry.as(Extensions.ViewsRegistry).onViewsRegistered(e => { + for (const view of e) { + for (const viewDescriptor of view.views) { + if (viewDescriptor.accessibilityHelpContent) { + this._viewHelpDialogMap.set(viewDescriptor.id, registerAccessibilityHelpAction(keybindingService, viewDescriptor)); + } + } + } + })); + this._register(Registry.as(Extensions.ViewsRegistry).onViewsDeregistered(e => { + for (const viewDescriptor of e.views) { + if (viewDescriptor.accessibilityHelpContent) { + this._viewHelpDialogMap.get(viewDescriptor.id)?.dispose(); + } + } + })); + } +} + +function registerAccessibilityHelpAction(keybindingService: IKeybindingService, viewDescriptor: IViewDescriptor): IDisposable { + const disposableStore = new DisposableStore(); + const content = viewDescriptor.accessibilityHelpContent?.value; + if (!content) { + throw new Error('No content provided for the accessibility help dialog'); + } + disposableStore.add(AccessibleViewRegistry.register({ + priority: 95, + name: viewDescriptor.id, + type: AccessibleViewType.Help, + when: FocusedViewContext.isEqualTo(viewDescriptor.id), + getProvider: (accessor: ServicesAccessor) => { + const viewsService = accessor.get(IViewsService); + return new ExtensionContentProvider( + viewDescriptor.id, + { type: AccessibleViewType.Help }, + () => content, + () => viewsService.openView(viewDescriptor.id, true), + ); + } + })); + + disposableStore.add(keybindingService.onDidUpdateKeybindings(() => { + disposableStore.clear(); + disposableStore.add(registerAccessibilityHelpAction(keybindingService, viewDescriptor)); + })); + return disposableStore; +} diff --git a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts index 53ad46e7846..d689c02503e 100644 --- a/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts +++ b/src/vs/workbench/contrib/accessibility/common/accessibilityCommands.ts @@ -12,5 +12,7 @@ export const enum AccessibilityCommandId { ShowPrevious = 'editor.action.accessibleViewPrevious', AccessibleViewAcceptInlineCompletion = 'editor.action.accessibleViewAcceptInlineCompletion', NextCodeBlock = 'editor.action.accessibleViewNextCodeBlock', - PreviousCodeBlock = 'editor.action.accessibleViewPreviousCodeBlock' + PreviousCodeBlock = 'editor.action.accessibleViewPreviousCodeBlock', + AccessibilityHelpConfigureKeybindings = 'editor.action.accessibilityHelpConfigureKeybindings', + AccessibilityHelpOpenHelpLink = 'editor.action.accessibilityHelpOpenHelpLink', } diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts index cd4ef189002..de1c2f798af 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts @@ -53,7 +53,7 @@ export class EditorTextPropertySignalsContribution extends Disposable implements constructor( @IEditorService private readonly _editorService: IEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, + @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService ) { super(); @@ -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._getDelay(signal, modality) + (didType.get() ? 1000 : 0); + const delay = this._accessibilitySignalService.getDelayMs(signal, modality) + (didType.get() ? 1000 : 0); timeouts.add(disposableTimeout(() => { if (source.isPresent(position, mode, undefined)) { @@ -162,23 +162,6 @@ export class EditorTextPropertySignalsContribution extends Disposable implements } })); } - - private _getDelay(signal: AccessibilitySignal, modality: AccessibilityModality): number { - // TODO make these delays configurable! - if (signal === AccessibilitySignal.errorAtPosition || signal === AccessibilitySignal.warningAtPosition) { - if (modality === 'sound') { - return 100; - } else { - return 1000; - } - } - - if (modality === 'sound') { - return 400; - } else { - return 3000; - } - } } interface TextProperty { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index e225605c017..d7ec863b045 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -4,62 +4,58 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { format } from 'vs/base/common/strings'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibleDiffViewerNext } from 'vs/editor/browser/widget/diffEditor/commands'; import { INLINE_CHAT_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE, CONTEXT_REQUEST } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; -export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'panelChat' | 'inlineChat'): string { - const keybindingService = accessor.get(IKeybindingService); +export class ChatAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 105; + readonly name = 'panelChat'; + readonly type = AccessibleViewType.Help; + readonly when = ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE, CONTEXT_REQUEST); + getProvider(accessor: ServicesAccessor) { + const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); + return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'panelChat'); + } +} + +export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat'): string { const content = []; - const openAccessibleViewKeybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); if (type === 'panelChat') { content.push(localize('chat.overview', 'The chat view is comprised of an input box and a request/response list. The input box is used to make requests and the list is used to display responses.')); content.push(localize('chat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.')); - content.push(openAccessibleViewKeybinding ? localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view {0}', openAccessibleViewKeybinding) : localize('chat.inspectResponseNoKb', 'With the input box focused, inspect the last response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); + content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view')); content.push(localize('chat.followUp', 'In the input box, navigate to the suggested follow up question (Shift+Tab) and press Enter to run it.')); content.push(localize('chat.announcement', 'Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.')); - content.push(descriptionForCommand('chat.action.focus', localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command ({0}).',), localize('workbench.action.chat.focusNoKb', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke The Focus Chat List command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.focusInput', localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command ({0}).'), localize('workbench.action.interactiveSession.focusInputNoKb', 'To focus the input box for chat requests, invoke the Focus Chat Input command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.nextCodeBlock', localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command ({0}).'), localize('workbench.action.chat.nextCodeBlockNoKb', 'To focus the next code block within a response, invoke the Chat: Next Code Block command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.nextFileTree', localize('workbench.action.chat.nextFileTree', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command ({0}).'), localize('workbench.action.chat.nextFileTreeNoKb', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command, which is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.clear', localize('workbench.action.chat.clear', 'To clear the request/response list, invoke the Chat Clear command ({0}).'), localize('workbench.action.chat.clearNoKb', 'To clear the request/response list, invoke the Chat Clear command, which is currently not triggerable by a keybinding.'), keybindingService)); + content.push(localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command.')); + content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command.')); + content.push(localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command.')); + content.push(localize('workbench.action.chat.nextFileTree', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command.')); + content.push(localize('workbench.action.chat.clear', 'To clear the request/response list, invoke the Chat Clear command.')); } else { - const startChatKeybinding = keybindingService.lookupKeybinding('inlineChat.start')?.getAriaLabel(); content.push(localize('inlineChat.overview', "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect.")); - content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat ({0}).", startChatKeybinding)); - const upHistoryKeybinding = keybindingService.lookupKeybinding('inlineChat.previousFromHistory')?.getAriaLabel(); - const downHistoryKeybinding = keybindingService.lookupKeybinding('inlineChat.nextFromHistory')?.getAriaLabel(); - if (upHistoryKeybinding && downHistoryKeybinding) { - content.push(localize('inlineChat.requestHistory', 'In the input box, use {0} and {1} to navigate your request history. Edit input and use enter or the submit button to run a new request.', upHistoryKeybinding, downHistoryKeybinding)); - } - content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible view {0}.', openAccessibleViewKeybinding) : localize('inlineChat.inspectResponseNoKb', 'With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.')); + content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat.")); + content.push(localize('inlineChat.requestHistory', 'In the input box, use and to navigate your request history. Edit input and use enter or the submit button to run a new request.')); + content.push(localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible viewview')); content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands.")); content.push(localize('inlineChat.fix', "If a fix action is invoked, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing.")); - const diffReviewKeybinding = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - content.push(diffReviewKeybinding ? localize('inlineChat.diff', "Once in the diff editor, enter review mode with ({0}). Use up and down arrows to navigate lines with the proposed changes.", diffReviewKeybinding) : localize('inlineChat.diffNoKb', "Tab again to enter the Diff editor with the changes and enter review mode with the Go to Next Difference Command. Use Up/DownArrow to navigate lines with the proposed changes.")); + content.push(localize('inlineChat.diff', "Once in the diff editor, enter review mode with. Use up and down arrows to navigate lines with the proposed changes.", AccessibleDiffViewerNext.id)); content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more.")); } content.push(localize('chat.signals', "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring.")); return content.join('\n\n'); } -function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { - const kb = keybindingService.lookupKeybinding(commandId); - if (kb) { - return format(msg, kb.getAriaLabel()); - } - return format(noKbMsg, commandId); -} - -export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat'): Promise { +export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat') { const widgetService = accessor.get(IChatWidgetService); - const accessibleViewService = accessor.get(IAccessibleViewService); const inputEditor: ICodeEditor | undefined = type === 'panelChat' ? widgetService.lastFocusedWidget?.inputEditor : editor; if (!inputEditor) { @@ -72,8 +68,8 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi const cachedPosition = inputEditor.getPosition(); inputEditor.getSupportedActions(); - const helpText = getAccessibilityHelpText(accessor, type); - accessibleViewService.show({ + const helpText = getAccessibilityHelpText(type); + return { id: type === 'panelChat' ? AccessibleViewProviderId.Chat : AccessibleViewProviderId.InlineChat, verbositySettingKey: type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat, provideContent: () => helpText, @@ -90,5 +86,5 @@ export async function runAccessibilityHelpAction(accessor: ServicesAccessor, edi } }, options: { type: AccessibleViewType.Help } - }); + }; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index a85f6e9c47d..fa695e9d757 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -5,32 +5,26 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { localize, localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IsLinuxContext, IsWindowsContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { Registry } from 'vs/platform/registry/common/platform'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; 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, CONTEXT_REQUEST, CONTEXT_RESPONSE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +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 { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export interface IChatViewTitleActionContext { @@ -53,6 +47,15 @@ export interface IChatViewOpenOptions { * Whether the query is partial and will await more input from the user. */ isPartialQuery?: boolean; + /** + * Any previous chat requests and responses that should be shown in the chat view. + */ + previousRequests?: IChatViewOpenRequestEntry[]; +} + +export interface IChatViewOpenRequestEntry { + request: string; + response: string; } class OpenChatGlobalAction extends Action2 { @@ -76,10 +79,16 @@ class OpenChatGlobalAction extends Action2 { override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise { opts = typeof opts === 'string' ? { query: opts } : opts; + const chatService = accessor.get(IChatService); const chatWidget = await showChatView(accessor.get(IViewsService)); if (!chatWidget) { return; } + if (opts?.previousRequests?.length && chatWidget.viewModel) { + for (const { request, response } of opts.previousRequests) { + chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response }); + } + } if (opts?.query) { if (opts.isPartialQuery) { chatWidget.setInput(opts.query); @@ -227,20 +236,6 @@ export function registerChatActions() { } }); - class ChatAccessibilityHelpContribution extends Disposable { - static ID: 'chatAccessibilityHelpContribution'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(105, 'panelChat', async accessor => { - const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - runAccessibilityHelpAction(accessor, codeEditor ?? undefined, 'panelChat'); - }, ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE, CONTEXT_REQUEST))); - } - } - - const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); - workbenchRegistry.registerWorkbenchContribution(ChatAccessibilityHelpContribution, LifecyclePhase.Eventually); - registerAction2(class FocusChatInputAction extends Action2 { constructor() { super({ @@ -260,3 +255,11 @@ export function registerChatActions() { } }); } + +export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { + if (isRequestVM(item)) { + return (includeName ? `${item.username}: ` : '') + item.messageText; + } else { + return (includeName ? `${item.username}: ` : '') + item.response.asString(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts new file mode 100644 index 00000000000..d63dd9fe8b7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.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 { CancellationToken } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Schemas } from 'vs/base/common/network'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI } from 'vs/base/common/uri'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { Command } from 'vs/editor/common/languages'; +import { localize, localize2 } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { AnythingQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickInputService, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; +import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; + +export function registerChatContextActions() { + registerAction2(AttachContextAction); +} + +export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem; + +export interface IFileQuickPickItem extends IQuickPickItem { + id: string; + name: string; + value: URI; + isDynamic: true; + + resource: URI; +} + +export interface IDynamicVariableQuickPickItem extends IQuickPickItem { + id: string; + name?: string; + value: unknown; + isDynamic: true; + + icon?: ThemeIcon; + command?: Command; +} + +export interface IStaticVariableQuickPickItem extends IQuickPickItem { + id: string; + name: string; + value: unknown; + isDynamic?: false; + + icon?: ThemeIcon; +} + +class AttachContextAction extends Action2 { + + static readonly ID = 'workbench.action.chat.attachContext'; + + constructor() { + super({ + id: AttachContextAction.ID, + title: localize2('workbench.action.chat.attachContext.label', "Attach Context"), + icon: Codicon.attach, + category: CHAT_CATEGORY, + keybinding: { + when: CONTEXT_IN_CHAT_INPUT, + primary: KeyMod.CtrlCmd | KeyCode.Slash, + weight: KeybindingWeight.EditorContrib + }, + menu: [ + { + when: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), + id: MenuId.ChatExecuteSecondary, + group: 'group_1', + }, + { + when: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), + id: MenuId.ChatExecute, + group: 'navigation', + }, + ] + }); + } + + private _getFileContextId(item: { resource: URI }) { + return item.resource.toString(); + } + + private async _attachContext(widget: IChatWidget, commandService: ICommandService, ...picks: IChatContextQuickPickItem[]) { + const toAttach: IChatRequestVariableEntry[] = []; + for (const pick of picks) { + if (pick && typeof pick === 'object' && 'command' in pick && pick.command) { + // Dynamic variable with a followup command + const selection = await commandService.executeCommand(pick.command.id, ...(pick.command.arguments ?? [])); + if (!selection) { + // User made no selection, skip this variable + continue; + } + toAttach.push({ + ...pick, + isDynamic: pick.isDynamic, + value: pick.value, + name: `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`, + // Apply the original icon with the new name + fullName: `${pick.icon ? `$(${pick.icon.id}) ` : ''}${selection}` + }); + } else if (pick && typeof pick === 'object' && 'resource' in pick) { + // #file variable + toAttach.push({ + ...pick, + id: this._getFileContextId(pick), + value: pick.resource, + name: pick.label, + isDynamic: true + }); + } else { + // All other dynamic variables and static variables + toAttach.push({ + ...pick, + id: pick.id, + value: pick.value, + fullName: pick.label, + name: 'name' in pick && typeof pick.name === 'string' ? pick.name : pick.label, + icon: 'icon' in pick && ThemeIcon.isThemeIcon(pick.icon) ? pick.icon : undefined + }); + } + } + + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, ...toAttach); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const quickInputService = accessor.get(IQuickInputService); + const chatAgentService = accessor.get(IChatAgentService); + const chatVariablesService = accessor.get(IChatVariablesService); + const commandService = accessor.get(ICommandService); + const widgetService = accessor.get(IChatWidgetService); + const context: { widget?: IChatWidget } | undefined = args[0]; + const widget = context?.widget ?? widgetService.lastFocusedWidget; + if (!widget) { + return; + } + + const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); + const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; + const quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] = []; + for (const variable of chatVariablesService.getVariables()) { + if (variable.fullName && (!variable.isSlow || slowSupported)) { + quickPickItems.push({ + label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, + name: variable.name, + id: variable.id, + icon: variable.icon + }); + } + } + + if (widget.viewModel?.sessionId) { + const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); + if (agentPart) { + const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); + for (const variable of completions) { + if (variable.fullName) { + quickPickItems.push({ + label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, + id: variable.id, + command: variable.command, + icon: variable.icon, + value: variable.value, + isDynamic: true, + name: variable.name + }); + } + } + } + + } + + if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { + quickPickItems.push(SelectAndInsertFileAction.Item, { type: 'separator' }); + } + + quickInputService.quickAccess.show('', { + enabledProviderPrefixes: [AnythingQuickAccessProvider.PREFIX], + placeholder: localize('chatContext.attach.placeholder', 'Search attachments'), + providerOptions: { + handleAccept: (item: IChatContextQuickPickItem) => { + this._attachContext(widget, commandService, item); + }, + additionPicks: quickPickItems, + includeSymbols: false, + filter: (item: IChatContextQuickPickItem) => { + // Avoid attaching the same context twice + const attachedContext = widget.getContrib(ChatContextAttachments.ID)?.getContext() ?? new Set(); + + if (item && typeof item === 'object' && 'resource' in item && URI.isUri(item.resource)) { + return [Schemas.file, Schemas.vscodeRemote].includes(item.resource.scheme) + && !attachedContext.has(this._getFileContextId({ resource: item.resource })); // Hack because Typescript doesn't narrow this type correctly + } + + if (!('command' in item)) { + return !attachedContext.has(item.id); + } + + // Don't filter out dynamic variables which show secondary data (temporary) + return true; + } + } + }); + + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index c55739a639a..5504c8e556f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -7,7 +7,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize2 } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { CHAT_CATEGORY, stringifyItem } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -72,11 +72,3 @@ export function registerChatCopyActions() { } }); } - -function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { - if (isRequestVM(item)) { - return (includeName ? `${item.username}: ` : '') + item.messageText; - } else { - return (includeName ? `${item.username}: ` : '') + item.response.asString(); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 8baf92e0312..4a2455ea71f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -8,16 +8,15 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { marked } from 'vs/base/common/marked/marked'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { localize, localize2 } from 'vs/nls'; -import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { localize2 } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { ChatTreeItem, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -57,10 +56,10 @@ export function registerChatTitleActions() { result: item.result, action: { kind: 'vote', - direction: InteractiveSessionVoteDirection.Up, + direction: ChatAgentVoteDirection.Up, } }); - item.setVote(InteractiveSessionVoteDirection.Up); + item.setVote(ChatAgentVoteDirection.Up); } }); @@ -96,10 +95,10 @@ export function registerChatTitleActions() { result: item.result, action: { kind: 'vote', - direction: InteractiveSessionVoteDirection.Down, + direction: ChatAgentVoteDirection.Down, } }); - item.setVote(InteractiveSessionVoteDirection.Down); + item.setVote(ChatAgentVoteDirection.Down); } }); @@ -254,89 +253,6 @@ export function registerChatTitleActions() { } } }); - - const rerunMenu = MenuId.for('ChatMessageTitle#Rerun'); - - MenuRegistry.appendMenuItem(MenuId.ChatMessageTitle, { - submenu: rerunMenu, - title: localize('reunmenu', "Rerun..."), - icon: Codicon.refresh, - group: 'navigation', - order: -10, - when: ContextKeyExpr.and(CONTEXT_RESPONSE, CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor)) // TODO@jrieken needs extension adoption - - }); - - registerAction2(class RerunAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.rerun', - title: localize2('chat.rerun.label', "Rerun Request"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.refresh, - precondition: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor), // TODO@jrieken needs extension adoption - menu: { - id: rerunMenu, - group: 'navigation', - order: -1, - } - }); - } - - async run(accessor: ServicesAccessor, ...args: [ChatTreeItem | unknown]) { - const chatWidgetService = accessor.get(IChatWidgetService); - const chatService = accessor.get(IChatService); - const widget = chatWidgetService.lastFocusedWidget; - let item = args[0]; - if (!isResponseVM(item)) { - item = widget?.getFocus(); - } - if (!isResponseVM(item) || !widget) { - return; - } - const request = chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); - if (request) { - await chatService.resendRequest(request, { noCommandDetection: false, attempt: request.attempt + 1, location: widget.location, implicitVariablesEnabled: true }); - } - } - }); - - registerAction2(class RerunWithoutCommandDetectionAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.rerunWithoutCommandDetection', - title: localize2('chat.rerunWithoutCommandDetection.label', "Rerun without Command Detection"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.refresh, - precondition: CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor), // TODO@jrieken needs extension adoption - menu: { - when: CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, - id: rerunMenu, - group: 'navigation', - order: -1, - } - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const chatWidgetService = accessor.get(IChatWidgetService); - const chatService = accessor.get(IChatService); - const widget = chatWidgetService.lastFocusedWidget; - let item = args[0]; - if (!isResponseVM(item)) { - item = widget?.getFocus(); - } - if (!isResponseVM(item) || !widget) { - return; - } - const request = chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); - if (request) { - await chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt, location: widget.location, implicitVariablesEnabled: true }); - } - } - }); } interface MarkdownContent { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index e6d70783da2..7d45fc86091 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; +import { MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isMacintosh } from 'vs/base/common/platform'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as nls from 'vs/nls'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -18,47 +18,45 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions'; +import { ChatAccessibilityHelp } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions'; -import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { agentSlashCommandToMarkdown, agentToMarkdown } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; +import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; +import { ChatResponseAccessibleView } from 'vs/workbench/contrib/chat/browser/chatResponseAccessibleView'; import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService'; -import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; -import { ChatAgentLocation, ChatAgentService, IChatAgentService, IChatAgentNameService, ChatAgentNameService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputCompletions'; +import { ChatAgentLocation, ChatAgentNameService, ChatAgentService, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { ILanguageModelsService, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; import { ILanguageModelStatsService, LanguageModelStatsService } from 'vs/workbench/contrib/chat/common/languageModelStats'; -import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; +import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChatService'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; -import { ChatExtensionPointHandler } from 'vs/workbench/contrib/chat/browser/chatParticipantContributions'; +import { registerChatContextActions } from 'vs/workbench/contrib/chat/browser/actions/chatContextActions'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -96,6 +94,7 @@ configurationRegistry.registerConfiguration({ 'chat.experimental.implicitContext': { type: 'boolean', description: nls.localize('chat.experimental.implicitContext', "Controls whether a checkbox is shown to allow the user to determine which implicit context is included with a chat participant's prompt."), + deprecated: true, default: false }, } @@ -143,89 +142,8 @@ class ChatResolverContribution extends Disposable { } } -class ChatAccessibleViewContribution extends Disposable { - static ID: 'chatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(100, 'panelChat', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const widgetService = accessor.get(IChatWidgetService); - const codeEditorService = accessor.get(ICodeEditorService); - return renderAccessibleView(accessibleViewService, widgetService, codeEditorService, true); - function renderAccessibleView(accessibleViewService: IAccessibleViewService, widgetService: IChatWidgetService, codeEditorService: ICodeEditorService, initialRender?: boolean): boolean { - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return false; - } - const chatInputFocused = initialRender && !!codeEditorService.getFocusedCodeEditor(); - if (initialRender && chatInputFocused) { - widget.focusLastMessage(); - } - - if (!widget) { - return false; - } - - const verifiedWidget: IChatWidget = widget; - const focusedItem = verifiedWidget.getFocus(); - - if (!focusedItem) { - return false; - } - - widget.focus(focusedItem); - const isWelcome = focusedItem instanceof ChatWelcomeMessageModel; - let responseContent = isResponseVM(focusedItem) ? focusedItem.response.asString() : undefined; - if (isWelcome) { - const welcomeReplyContents = []; - for (const content of focusedItem.content) { - if (Array.isArray(content)) { - welcomeReplyContents.push(...content.map(m => m.message)); - } else { - welcomeReplyContents.push((content as IMarkdownString).value); - } - } - responseContent = welcomeReplyContents.join('\n'); - } - if (!responseContent && 'errorDetails' in focusedItem && focusedItem.errorDetails) { - responseContent = focusedItem.errorDetails.message; - } - if (!responseContent) { - return false; - } - const responses = verifiedWidget.viewModel?.getItems().filter(i => isResponseVM(i)); - const length = responses?.length; - const responseIndex = responses?.findIndex(i => i === focusedItem); - - accessibleViewService.show({ - id: AccessibleViewProviderId.Chat, - verbositySettingKey: AccessibilityVerbositySettingId.Chat, - provideContent(): string { return responseContent!; }, - onClose() { - verifiedWidget.reveal(focusedItem); - if (chatInputFocused) { - verifiedWidget.focusInput(); - } else { - verifiedWidget.focus(focusedItem); - } - }, - next() { - verifiedWidget.moveFocus(focusedItem, 'next'); - alertFocusChange(responseIndex, length, 'next'); - renderAccessibleView(accessibleViewService, widgetService, codeEditorService); - }, - previous() { - verifiedWidget.moveFocus(focusedItem, 'previous'); - alertFocusChange(responseIndex, length, 'previous'); - renderAccessibleView(accessibleViewService, widgetService, codeEditorService); - }, - options: { type: AccessibleViewType.View } - }); - return true; - } - }, CONTEXT_IN_CHAT_SESSION)); - } -} +AccessibleViewRegistry.register(new ChatResponseAccessibleView()); +AccessibleViewRegistry.register(new ChatAccessibilityHelp()); class ChatSlashStaticSlashCommandsContribution extends Disposable { @@ -234,6 +152,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { @ICommandService commandService: ICommandService, @IChatAgentService chatAgentService: IChatAgentService, @IChatVariablesService chatVariablesService: IChatVariablesService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); this._store.add(slashCommandService.registerSlashCommand({ @@ -268,16 +187,12 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { .filter(a => a.id !== defaultAgent?.id) .filter(a => a.locations.includes(ChatAgentLocation.Panel)) .map(async a => { - const agentWithLeader = `${chatAgentLeader}${a.name}`; - const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest ?? ''}` }; - const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const description = a.description ? `- ${a.description}` : ''; - const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) ${description}`; + const agentMarkdown = instantiationService.invokeFunction(accessor => agentToMarkdown(a, true, accessor)); + const agentLine = `- ${agentMarkdown} ${description}`; const commandText = a.slashCommands.map(c => { - const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` }; - const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const description = c.description ? `- ${c.description}` : ''; - return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) ${description}`; + return `\t* ${agentSlashCommandToMarkdown(a, c)} ${description}`; }).join('\n'); return (agentLine + '\n' + commandText).trim(); @@ -318,7 +233,6 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribution, WorkbenchPhase.BlockStartup); -workbenchContributionsRegistry.registerWorkbenchContribution(ChatAccessibleViewContribution, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlashCommandsContribution, LifecyclePhase.Eventually); Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(ChatEditorInput.TypeID, ChatEditorInputSerializer); registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); @@ -334,6 +248,7 @@ registerQuickChatActions(); registerChatExportActions(); registerMoveActions(); registerNewChatActions(); +registerChatContextActions(); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 357ca063a54..48844b393d8 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -10,11 +10,13 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Selection } from 'vs/editor/common/core/selection'; import { localize } from 'vs/nls'; 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 { 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'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -126,8 +128,10 @@ export type IChatWidgetViewContext = IChatViewViewContext | IChatResourceViewCon export interface IChatWidget { readonly onDidChangeViewModel: Event; readonly onDidAcceptInput: Event; + readonly onDidHide: Event; readonly onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; readonly onDidChangeParsedInput: Event; + readonly onDidDeleteContext: Event; readonly location: ChatAgentLocation; readonly viewContext: IChatWidgetViewContext; readonly viewModel: IChatViewModel | undefined; @@ -135,6 +139,7 @@ export interface IChatWidget { readonly supportsFileReferences: boolean; readonly parsedInput: IParsedChatRequest; lastSelectedAgent: IChatAgentData | undefined; + readonly scopedContextKeyService: IContextKeyService; getContrib(id: string): T | undefined; reveal(item: ChatTreeItem): void; @@ -143,7 +148,7 @@ export interface IChatWidget { getFocus(): ChatTreeItem | undefined; setInput(query?: string): void; getInput(): string; - acceptInput(query?: string): void; + acceptInput(query?: string): Promise; acceptInputWithPrefix(prefix: string): void; setInputPlaceholder(placeholder: string): void; resetInputPlaceholder(): void; @@ -154,6 +159,7 @@ export interface IChatWidget { getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[]; getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[]; getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined; + setContext(overwrite: boolean, ...context: IChatRequestVariableEntry[]): void; clear(): void; } diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts index 933a8940869..5eb8edf7f6d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts @@ -8,7 +8,7 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { marked } from 'vs/base/common/marked/marked'; import { localize } from 'vs/nls'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; import { isRequestVM, isResponseVM, isWelcomeVM, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index aa7c612e090..304fb89dbc9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -10,6 +10,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService { @@ -34,11 +35,11 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.asString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); - if (!response) { + if (!response || !responseContent) { return; } const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : ''; - status(responseContent + errorDetails); + status(renderStringAsPlaintext(responseContent) + errorDetails); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts index 55e72653021..9163fd24fdb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAgentHover.ts @@ -5,16 +5,17 @@ import * as dom from 'vs/base/browser/dom'; import { h } from 'vs/base/browser/dom'; -import { Button } from 'vs/base/browser/ui/button/button'; +import { IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { showExtensionsWithIdsCommandId } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { verifiedPublisherIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -28,12 +29,10 @@ export class ChatAgentHover extends Disposable { private readonly publisherName: HTMLElement; private readonly description: HTMLElement; - private currentAgent: IChatAgentData | undefined; - constructor( @IChatAgentService private readonly chatAgentService: IChatAgentService, @IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService, - @ICommandService private readonly commandService: ICommandService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, ) { super(); @@ -51,8 +50,8 @@ export class ChatAgentHover extends Disposable { ]), ]), ]), + h('.chat-agent-hover-warning@warning'), h('span.chat-agent-hover-description@description'), - h('span.chat-agent-hover-marketplace-button@button'), ]); this.domNode = hoverElement.root; @@ -71,30 +70,12 @@ export class ChatAgentHover extends Disposable { verifiedBadge, this.publisherName); - const label = localize('marketplaceLabel', "View in Marketplace") + '.'; - const marketplaceButton = this._register(new Button(hoverElement.button, { - title: label, - buttonBackground: undefined, - buttonBorder: undefined, - buttonForeground: undefined, - buttonHoverBackground: undefined, - buttonSecondaryBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined, - })); - marketplaceButton.label = label; - this._register(marketplaceButton.onDidClick(() => { - if (this.currentAgent) { - this.commandService.executeCommand(showExtensionsWithIdsCommandId, [this.currentAgent.extensionId.value]); - } - })); + hoverElement.warning.appendChild(renderIcon(Codicon.warning)); + hoverElement.warning.appendChild(dom.$('span', undefined, localize('reservedName', "This chat extension is using a reserved name."))); } setAgent(id: string): void { const agent = this.chatAgentService.getAgent(id)!; - this.currentAgent = agent; - if (agent.metadata.icon instanceof URI) { const avatarIcon = dom.$('img.icon'); avatarIcon.src = FileAccess.uriToBrowserUri(agent.metadata.icon).toString(true); @@ -106,18 +87,20 @@ export class ChatAgentHover extends Disposable { this.domNode.classList.toggle('noExtensionName', !!agent.isDynamic); - this.name.textContent = `@${agent.name}`; + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent); + this.name.textContent = isAllowed ? `@${agent.name}` : getFullyQualifiedId(agent); this.extensionName.textContent = agent.extensionDisplayName; this.publisherName.textContent = agent.publisherDisplayName ?? agent.extensionPublisherId; let description = agent.description ?? ''; if (description) { - if (!description.match(/\. *$/)) { + if (!description.match(/[\.\?\!] *$/)) { description += '.'; } } this.description.textContent = description; + this.domNode.classList.toggle('allowedName', isAllowed); this.domNode.classList.toggle('verifiedPublisher', false); if (!agent.isDynamic) { @@ -132,3 +115,20 @@ export class ChatAgentHover extends Disposable { } } } + +export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefined, commandService: ICommandService): IUpdatableHoverOptions { + return { + actions: [ + { + commandId: showExtensionsWithIdsCommandId, + label: localize('viewExtensionLabel', "View Extension"), + run: () => { + const agent = getAgent(); + if (agent) { + commandService.executeCommand(showExtensionsWithIdsCommandId, [agent.extensionId.value]); + } + }, + } + ] + }; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts new file mode 100644 index 00000000000..ea5cf39c113 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatConfirmationWidget.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'vs/css!./media/chatConfirmationWidget'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { Emitter, Event } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; + +export interface IChatConfirmationButton { + label: string; + isSecondary?: boolean; + data: any; +} + +export class ChatConfirmationWidget extends Disposable { + private _onDidClick = this._register(new Emitter()); + get onDidClick(): Event { return this._onDidClick.event; } + + private _domNode: HTMLElement; + get domNode(): HTMLElement { + return this._domNode; + } + + setShowButtons(showButton: boolean): void { + this.domNode.classList.toggle('hideButtons', !showButton); + } + + constructor( + title: string, + message: string, + buttons: IChatConfirmationButton[], + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + const elements = dom.h('.chat-confirmation-widget@root', [ + dom.h('.chat-confirmation-widget-title@title'), + dom.h('.chat-confirmation-widget-message@message'), + dom.h('.chat-confirmation-buttons-container@buttonsContainer'), + ]); + this._domNode = elements.root; + const renderer = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); + + const renderedTitle = this._register(renderer.render(new MarkdownString(title))); + elements.title.appendChild(renderedTitle.element); + + const renderedMessage = this._register(renderer.render(new MarkdownString(message))); + elements.message.appendChild(renderedMessage.element); + + buttons.forEach(buttonData => { + const button = new Button(elements.buttonsContainer, { ...defaultButtonStyles, secondary: buttonData.isSecondary }); + button.label = buttonData.label; + this._register(button.onDidClick(() => this._onDidClick.fire(buttonData))); + }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 875863c2e73..1da5d57b810 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -75,6 +75,12 @@ export class ChatEditor extends EditorPane { this.widget.setVisible(true); } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + + this.widget?.setVisible(visible); + } + public override focus(): void { super.focus(); diff --git a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts index 29a5ba75b7e..4f59229ff85 100644 --- a/src/vs/workbench/contrib/chat/browser/chatFollowups.ts +++ b/src/vs/workbench/contrib/chat/browser/chatFollowups.ts @@ -8,22 +8,19 @@ import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; -import { IInlineChatFollowup } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; const $ = dom.$; -export class ChatFollowups extends Disposable { +export class ChatFollowups extends Disposable { constructor( container: HTMLElement, followups: T[], private readonly location: ChatAgentLocation, private readonly options: IButtonStyles | undefined, private readonly clickHandler: (followup: T) => void, - @IContextKeyService private readonly contextService: IContextKeyService, @IChatAgentService private readonly chatAgentService: IChatAgentService ) { super(); @@ -34,10 +31,6 @@ export class ChatFollowups extend private renderFollowup(container: HTMLElement, followup: T): void { - if (followup.kind === 'command' && followup.when && !this.contextService.contextMatchesRules(ContextKeyExpr.deserialize(followup.when))) { - return; - } - if (!this.chatAgentService.getDefaultAgent(this.location)) { // No default agent yet, which affects how followups are rendered, so can't render this yet return; @@ -60,23 +53,17 @@ export class ChatFollowups extend const baseTitle = followup.kind === 'reply' ? (followup.title || followup.message) : followup.title; - - const tooltip = tooltipPrefix + - ('tooltip' in followup && followup.tooltip || baseTitle); - const button = this._register(new Button(container, { ...this.options, supportIcons: true, title: tooltip })); + const message = followup.kind === 'reply' ? followup.message : followup.title; + const tooltip = (tooltipPrefix + + ('tooltip' in followup && followup.tooltip || message)).trim(); + const button = this._register(new Button(container, { ...this.options, title: tooltip })); if (followup.kind === 'reply') { button.element.classList.add('interactive-followup-reply'); } else if (followup.kind === 'command') { button.element.classList.add('interactive-followup-command'); } - button.element.ariaLabel = localize('followUpAriaLabel', "Follow up question: {0}", followup.title); - let label = ''; - if (followup.kind === 'reply') { - label = '$(sparkle) ' + baseTitle; - } else { - label = baseTitle; - } - button.label = new MarkdownString(label, { supportThemeIcons: true }); + button.element.ariaLabel = localize('followUpAriaLabel', "Follow up question: {0}", baseTitle); + button.label = new MarkdownString(baseTitle); this._register(button.onDidClick(() => this.clickHandler(followup))); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 211619b3336..dc56a5c97aa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -7,7 +7,8 @@ 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 * as aria from 'vs/base/browser/ui/aria/aria'; -import { Checkbox } from 'vs/base/browser/ui/toggle/toggle'; +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'; @@ -32,14 +33,14 @@ import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { FileKind } from 'vs/platform/files/common/files'; import { registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { defaultCheckboxStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { asCssVariableWithDefault, checkboxBorder, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ResourceLabels } from 'vs/workbench/browser/labels'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { CancelAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; @@ -47,6 +48,7 @@ import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_FOCUS, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; 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'; @@ -83,9 +85,22 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _onDidBlur = this._register(new Emitter()); readonly onDidBlur = this._onDidBlur.event; + private _onDidDeleteContext = this._register(new Emitter()); + readonly onDidDeleteContext = this._onDidDeleteContext.event; + private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>()); readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; + public get attachedContext() { + return this._attachedContext; + } + + private readonly _attachedContext = new Set(); + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); + + private readonly inputEditorMaxHeight: number; private inputEditorHeight = 0; private container!: HTMLElement; @@ -94,13 +109,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private followupsContainer!: HTMLElement; private readonly followupsDisposables = this._register(new DisposableStore()); - private implicitContextContainer!: HTMLElement; - private implicitContextLabel!: HTMLElement; - private implicitContextCheckbox!: Checkbox; - private implicitContextSettingEnabled = false; - get implicitContextEnabled() { - return this.implicitContextCheckbox.checked; - } + private attachedContextContainer!: HTMLElement; + private readonly attachedContextDisposables = this._register(new DisposableStore()); private _inputPartHeight: number = 0; get inputPartHeight() { @@ -145,6 +155,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ) { super(); + this.inputEditorMaxHeight = this.options.renderStyle === 'compact' ? INPUT_EDITOR_MAX_HEIGHT / 3 : INPUT_EDITOR_MAX_HEIGHT; + this.inputEditorHasText = CONTEXT_CHAT_INPUT_HAS_TEXT.bindTo(contextKeyService); this.chatCursorAtTop = CONTEXT_CHAT_INPUT_CURSOR_AT_TOP.bindTo(contextKeyService); this.inputEditorHasFocus = CONTEXT_CHAT_INPUT_HAS_FOCUS.bindTo(contextKeyService); @@ -152,15 +164,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.history = new HistoryNavigator([], 5); this._register(this.historyService.onDidClearHistory(() => this.history.clear())); - this.implicitContextSettingEnabled = this.configurationService.getValue('chat.experimental.implicitContext'); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { this.inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() }); } - - if (e.affectsConfiguration('chat.experimental.implicitContext')) { - this.implicitContextSettingEnabled = this.configurationService.getValue('chat.experimental.implicitContext'); - } })); } @@ -174,7 +181,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } setState(inputValue: string | undefined): void { - const history = this.historyService.getHistory(); + const history = this.historyService.getHistory(this.location); this.history = new HistoryNavigator(history, 50); if (typeof inputValue === 'string') { @@ -182,6 +189,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } + setVisible(visible: boolean): void { + this._onDidChangeVisibility.fire(visible); + } + get element(): HTMLElement { return this.container; } @@ -270,13 +281,21 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputEditor.focus(); } + attachContext(...contentReferences: IChatRequestVariableEntry[]): void { + for (const reference of contentReferences) { + this.attachedContext.add(reference); + } + + this.initAttachedContext(this.attachedContextContainer); + } + render(container: HTMLElement, initialValue: string, widget: IChatWidget) { this.container = dom.append(container, $('.interactive-input-part')); this.container.classList.toggle('compact', this.options.renderStyle === 'compact'); this.followupsContainer = dom.append(this.container, $('.interactive-input-followups')); - this.implicitContextContainer = dom.append(this.container, $('.chat-implicit-context')); - this.initImplicitContext(this.implicitContextContainer); + this.attachedContextContainer = dom.append(this.container, $('.chat-attached-context')); + this.initAttachedContext(this.attachedContextContainer); const inputAndSideToolbar = dom.append(this.container, $('.interactive-input-and-side-toolbar')); const inputContainer = dom.append(inputAndSideToolbar, $('.interactive-input-and-execute-toolbar')); @@ -314,7 +333,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); this._register(this._inputEditor.onDidChangeModelContent(() => { - const currentHeight = Math.min(this._inputEditor.getContentHeight(), INPUT_EDITOR_MAX_HEIGHT); + const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); if (currentHeight !== this.inputEditorHeight) { this.inputEditorHeight = currentHeight; this._onDidChangeHeight.fire(); @@ -415,16 +434,35 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - private initImplicitContext(container: HTMLElement) { - this.implicitContextCheckbox = new Checkbox('#selection', true, { ...defaultCheckboxStyles, checkboxBorder: asCssVariableWithDefault(checkboxBorder, inputBackground) }); - container.append(this.implicitContextCheckbox.domNode); - this.implicitContextLabel = dom.append(container, $('span.chat-implicit-context-label')); - this.implicitContextLabel.textContent = '#selection'; - } + private initAttachedContext(container: HTMLElement) { + dom.clearNode(container); + this.attachedContextDisposables.clear(); + dom.setVisibility(Boolean(this.attachedContext.size), this.attachedContextContainer); + for (const attachment of this.attachedContext) { + const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons')); + const label = this._contextResourceLabels.create(widget, { supportIcons: true }); + const file = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; + if (file) { + label.setFile(file, { + fileKind: FileKind.FILE, + hidePath: true, + range: attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined, + }); + } else { + label.setLabel(attachment.fullName ?? attachment.name); + } - setImplicitContextKinds(kinds: string[]) { - dom.setVisibility(this.implicitContextSettingEnabled && kinds.length > 0, this.implicitContextContainer); - this.implicitContextLabel.textContent = localize('use', "Use") + ' ' + kinds.map(k => `#${k}`).join(', '); + const clearButton = new Button(widget, { supportIcons: true }); + this.attachedContextDisposables.add(clearButton); + clearButton.icon = Codicon.close; + const disp = clearButton.onDidClick(() => { + this.attachedContext.delete(attachment); + disp.dispose(); + this._onDidChangeHeight.fire(); + this._onDidDeleteContext.fire(attachment); + }); + this.attachedContextDisposables.add(disp); + } } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { @@ -452,6 +490,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private previousInputEditorDimension: IDimension | undefined; private _layout(height: number, width: number, allowRecurse = true): void { + this.initAttachedContext(this.attachedContextContainer); const data = this.getLayoutData(); @@ -479,10 +518,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return { inputEditorBorder: 2, followupsHeight: this.followupsContainer.offsetHeight, - inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), INPUT_EDITOR_MAX_HEIGHT), + inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight), inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 8 : 40, inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 24, - implicitContextHeight: this.implicitContextContainer.offsetHeight, + implicitContextHeight: this.attachedContextContainer.offsetHeight, editorBorder: 2, editorPadding: 12, toolbarPadding: 4, @@ -493,7 +532,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge saveState(): void { const inputHistory = this.history.getHistory(); - this.historyService.saveHistory(inputHistory); + this.historyService.saveHistory(this.location, inputHistory); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index a05023440ee..66363a9abc6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ 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'; @@ -16,28 +18,29 @@ import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { IAsyncDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; import { distinct } from 'vs/base/common/arrays'; -import { disposableTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; 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 { ResourceMap } from 'vs/base/common/map'; import { FileAccess, Schemas, matchesSomeScheme } from 'vs/base/common/network'; import { clamp } from 'vs/base/common/numbers'; -import { IObservable, autorun, constObservable } from 'vs/base/common/observable'; +import { autorun } from 'vs/base/common/observable'; import { basename } from 'vs/base/common/path'; import { basenameOrAuthority } from 'vs/base/common/resources'; import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; -import { isUndefined } from 'vs/base/common/types'; 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 { localize } from 'vs/nls'; import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -58,27 +61,26 @@ 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 } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatAgentHover, getChatAgentHoverOptions } from 'vs/workbench/contrib/chat/browser/chatAgentHover'; +import { ChatConfirmationWidget } from 'vs/workbench/contrib/chat/browser/chatConfirmationWidget'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer'; 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 { ChatAgentLocation, IChatAgentMetadata, IChatAgentNameService } from 'vs/workbench/contrib/chat/common/chatAgents'; +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 { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +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, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +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 { CodeBlockModelCollection } from '../common/codeBlockModelCollection'; import { IChatListItemRendererOptions } from './chat'; -import { DefaultModelSHA1Computer } from 'vs/editor/common/services/modelService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; +import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; const $ = dom.$; @@ -87,7 +89,6 @@ interface IChatListItemTemplate { readonly rowContainer: HTMLElement; readonly titleToolbar?: MenuWorkbenchToolBar; readonly avatarContainer: HTMLElement; - readonly agentAvatarContainer: HTMLElement; readonly username: HTMLElement; readonly detail: HTMLElement; readonly value: HTMLElement; @@ -126,6 +127,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); readonly onDidClickFollowup: Event = this._onDidClickFollowup.event; + private readonly _onDidClickRerunWithAgentOrCommandDetection = new Emitter(); + readonly onDidClickRerunWithAgentOrCommandDetection: Event = this._onDidClickRerunWithAgentOrCommandDetection.event; + protected readonly _onDidChangeItemHeight = this._register(new Emitter()); readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; @@ -156,13 +160,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + const hoverContent = () => { if (isResponseVM(template.currentElement) && template.currentElement.agent) { agentHover.setAgent(template.currentElement.agent.id); return agentHover.domNode; } return undefined; + }; + const hoverOptions = getChatAgentHoverOptions(() => isResponseVM(template.currentElement) ? template.currentElement.agent : undefined, this.commandService); + templateDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('element'), user, hoverContent, hoverOptions)); + templateDisposables.add(this.hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), header, hoverContent, hoverOptions)); + templateDisposables.add(dom.addDisposableListener(user, dom.EventType.KEY_DOWN, e => { + const ev = new StandardKeyboardEvent(e); + if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) { + const content = hoverContent(); + if (content) { + this.hoverService.showHover({ content, target: user, trapFocus: true, actions: hoverOptions.actions }, true); + } + } else if (ev.equals(KeyCode.Escape)) { + this.hoverService.hideHover(); + } })); - - const template: IChatListItemTemplate = { avatarContainer, agentAvatarContainer, username, detail, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService, agentHover }; + const template: IChatListItemTemplate = { avatarContainer, username, detail, referencesListContainer, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService, agentHover }; return template; } @@ -328,7 +344,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer = constObservable(undefined); - - if (element.agent && !element.agent.isDefault) { - const name = element.agent.name; - agentName = this.chatAgentNameService.getAgentNameRestriction(element.agent) - .map(allowed => allowed ? name : name); // TODO - } - templateData.elementDisposables.add(autorun(reader => { - this._renderDetail(element, agentName.read(reader), templateData); + this._renderDetail(element, templateData); })); } - private _renderDetail(element: IChatResponseViewModel, agentName: string | undefined, templateData: IChatListItemTemplate): void { - let progressMsg: string = ''; - if (!isUndefined(agentName)) { - let usingMsg = chatAgentLeader + agentName; - if (element.slashCommand) { - usingMsg += ` ${chatSubcommandLeader}${element.slashCommand.name}`; - } + private _renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { + dom.clearNode(templateData.detail); + + if (element.slashCommand && element.agentOrSlashCommandDetected) { + let msg: string = ''; + const usingMsg = `${chatSubcommandLeader}${element.slashCommand.name}`; if (element.isComplete) { - progressMsg = localize('usedAgent', "used {0}", usingMsg); + msg = localize('usedAgent', "used {0} [[(rerun without)]]", usingMsg); } else { - progressMsg = localize('usingAgent', "using {0}", usingMsg); + msg = localize('usingAgent', "using {0}", usingMsg); } - } else if (element.agentOrSlashCommandDetected) { - const usingMsg: string[] = []; - if (!isUndefined(agentName)) { - usingMsg.push(chatAgentLeader + agentName); - } - if (element.slashCommand) { - usingMsg.push(chatSubcommandLeader + element.slashCommand.name); - } - if (usingMsg.length) { - if (element.isComplete) { - progressMsg = localize('usedAgent', "used {0}", usingMsg.join(' ')); - } else { - progressMsg = localize('usingAgent', "using {0}", usingMsg.join(' ')); + dom.reset(templateData.detail, renderFormattedText(msg, { + className: 'agentOrSlashCommandDetected', + inline: true, + actionHandler: { + disposables: templateData.elementDisposables, + callback: (content) => { + this._onDidClickRerunWithAgentOrCommandDetection.fire(element); + }, } - } - } else if (!element.isComplete) { - progressMsg = GeneratingPhrase; - } + })); - templateData.detail.textContent = progressMsg; + } else if (!element.isComplete) { + templateData.detail.textContent = GeneratingPhrase; + } } private renderAvatar(element: ChatTreeItem, templateData: IChatListItemTemplate): void { - if (URI.isUri(element.avatarIcon)) { - const avatarImgIcon = dom.$('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIcon).toString(true); - templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarImgIcon)); + const icon = isResponseVM(element) ? + this.getAgentIcon(element.agent?.metadata) : + (element.avatarIcon ?? Codicon.account); + if (icon instanceof URI) { + const avatarIcon = dom.$('img.icon'); + avatarIcon.src = FileAccess.uriToBrowserUri(icon).toString(true); + templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); } else { - const defaultIcon = isRequestVM(element) ? Codicon.account : Codicon.copilot; - const icon = element.avatarIcon ?? defaultIcon; const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); } - - if (isResponseVM(element) && element.agent && !element.agent.isDefault) { - dom.show(templateData.agentAvatarContainer); - const icon = this.getAgentIcon(element.agent.metadata); - if (icon instanceof URI) { - const avatarIcon = dom.$('img.icon'); - avatarIcon.src = FileAccess.uriToBrowserUri(icon).toString(true); - templateData.agentAvatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); - } else if (icon) { - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); - templateData.agentAvatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); - } else { - dom.hide(templateData.agentAvatarContainer); - return; - } - - templateData.agentAvatarContainer.classList.toggle('complete', element.isComplete); - if (!element.agentAvatarHasBeenRendered && !element.isComplete) { - element.agentAvatarHasBeenRendered = true; - templateData.agentAvatarContainer.classList.remove('loading'); - templateData.elementDisposables.add(disposableTimeout(() => { - templateData.agentAvatarContainer.classList.toggle('loading', !element.isComplete); - }, 100)); - } else { - templateData.agentAvatarContainer.classList.toggle('loading', !element.isComplete); - } - } else { - dom.hide(templateData.agentAvatarContainer); - } } - private getAgentIcon(agent: IChatAgentMetadata): URI | ThemeIcon | undefined { - if (agent.themeIcon) { + private getAgentIcon(agent: IChatAgentMetadata | undefined): URI | ThemeIcon { + if (agent?.themeIcon) { return agent.themeIcon; + } else if (agent?.iconDark && this.themeService.getColorTheme().type === ColorScheme.DARK) { + return agent.iconDark; + } else if (agent?.icon) { + return agent.icon; } else { - return this.themeService.getColorTheme().type === ColorScheme.DARK && agent.iconDark ? agent.iconDark : - agent.icon; + return Codicon.copilot; } } @@ -511,10 +487,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + // Have to recompute the height here because codeblock rendering is currently async and it may have changed. + // If it becomes properly sync, then this could be removed. + element.currentRenderedHeight = templateData.rowContainer.offsetHeight; disposable.dispose(); - this._onDidChangeItemHeight.fire({ element, height: newHeight }); + this._onDidChangeItemHeight.fire({ element, height: element.currentRenderedHeight }); })); } } + private updateItemHeight(templateData: IChatListItemTemplate): void { + if (!templateData.currentElement) { + return; + } + + const newHeight = templateData.rowContainer.offsetHeight; + templateData.currentElement.currentRenderedHeight = newHeight; + this._onDidChangeItemHeight.fire({ element: templateData.currentElement, height: newHeight }); + } + private renderWelcomeMessage(element: IChatWelcomeMessageViewModel, templateData: IChatListItemTemplate) { dom.clearNode(templateData.value); dom.clearNode(templateData.referencesListContainer); @@ -567,8 +558,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + // Have to recompute the height here because codeblock rendering is currently async and it may have changed. + // If it becomes properly sync, then this could be removed. + element.currentRenderedHeight = templateData.rowContainer.offsetHeight; disposable.dispose(); - this._onDidChangeItemHeight.fire({ element, height: newHeight }); + this._onDidChangeItemHeight.fire({ element, height: element.currentRenderedHeight }); })); } } @@ -608,10 +602,17 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer !('isSettled' in p) || !p.isSettled).length === 0 && !somePartIsNotFullyRendered; if (isFullyRendered && element.isComplete) { // Response is done and content is rendered, so do a normal render @@ -685,11 +693,16 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + this.updateItemHeight(templateData); })); treeDisposables.add(tree.onContextMenu((e) => { e.browserEvent.preventDefault(); @@ -760,7 +773,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { if (!ref.isStale()) { tree.layout(); - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + this.updateItemHeight(templateData); } }); @@ -792,22 +805,25 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, element: IChatResponseViewModel, templateData: IChatListItemTemplate): { element: HTMLElement; dispose: () => void } { + private renderContentReferencesListData(task: IChatTask | null, data: ReadonlyArray, element: IChatResponseViewModel, templateData: IChatListItemTemplate): { element: HTMLElement; dispose: () => void } { const listDisposables = new DisposableStore(); - const referencesLabel = data.length > 1 ? + const referencesLabel = task?.content.value ?? (data.length > 1 ? localize('usedReferencesPlural', "Used {0} references", data.length) : - localize('usedReferencesSingular', "Used {0} reference", 1); + 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))); @@ -825,7 +841,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { @@ -833,7 +849,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (e.element) { + 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; @@ -885,12 +901,27 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + if (isResponseVM(element)) { + const prompt = `${e.label}: "${confirmation.title}"`; + const data: IChatSendRequestOptions = e.isSecondary ? + { rejectedConfirmationData: [e.data] } : + { acceptedConfirmationData: [e.data] }; + data.agentId = element.agent?.id; + if (await this.chatService.sendRequest(element.sessionId, prompt, data)) { + confirmation.isUsed = true; + confirmationWidget.setShowButtons(false); + this.updateItemHeight(templateData); + } + } + })); + + return { + element: confirmationWidget.domNode, + dispose() { store.dispose(); } + }; + } + private renderTextEdit(element: ChatTreeItem, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IMarkdownRenderResult | undefined { // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen @@ -975,7 +1035,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { ref.object.layout(this._currentLayoutWidth); - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + this.updateItemHeight(templateData); })); const data: ICodeCompareBlockData = { @@ -1043,7 +1103,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.trustedDomainService.isValid(uri), fillInIncompleteTokens, codeBlockRendererSync: (languageId, text) => { const index = codeBlockIndex++; @@ -1077,7 +1136,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { ref.object.layout(this._currentLayoutWidth); - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); + this.updateItemHeight(templateData); })); if (isResponseVM(element)) { @@ -1098,7 +1157,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }), + asyncRenderCallback: () => this.updateItemHeight(templateData), }); if (isResponseVM(element)) { @@ -1325,9 +1384,9 @@ class TreePool extends Disposable { } class ContentReferencesListPool extends Disposable { - private _pool: ResourcePool>; + private _pool: ResourcePool>; - public get inUse(): ReadonlySet> { + public get inUse(): ReadonlySet> { return this._pool.inUse; } @@ -1340,14 +1399,14 @@ class ContentReferencesListPool extends Disposable { this._pool = this._register(new ResourcePool(() => this.listFactory())); } - private listFactory(): WorkbenchList { + 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, + WorkbenchList, 'ChatListRenderer', container, new ContentReferencesListDelegate(), @@ -1355,7 +1414,10 @@ class ContentReferencesListPool extends Disposable { { alwaysConsumeMouseWheel: false, accessibilityProvider: { - getAriaLabel: (element: IChatContentReference) => { + getAriaLabel: (element: IChatContentReference | IChatWarningMessage) => { + if (element.kind === 'warning') { + return element.content.value; + } const reference = element.reference; if ('variableName' in reference) { return reference.variableName; @@ -1369,7 +1431,11 @@ class ContentReferencesListPool extends Disposable { getWidgetAriaLabel: () => localize('usedReferences', "Used References") }, dnd: { - getDragURI: ({ reference }: IChatContentReference) => { + getDragURI: (element: IChatContentReference | IChatWarningMessage) => { + if (element.kind === 'warning') { + return null; + } + const { reference } = element; if ('variableName' in reference) { return null; } else if (URI.isUri(reference)) { @@ -1387,7 +1453,7 @@ class ContentReferencesListPool extends Disposable { return list; } - get(): IDisposableReference> { + get(): IDisposableReference> { const object = this._pool.get(); let stale = false; return { @@ -1401,7 +1467,7 @@ class ContentReferencesListPool extends Disposable { } } -class ContentReferencesListDelegate implements IListVirtualDelegate { +class ContentReferencesListDelegate implements IListVirtualDelegate { getHeight(element: IChatContentReference): number { return 22; } @@ -1416,7 +1482,7 @@ interface IChatContentReferenceListTemplate { templateDisposables: IDisposable; } -class ContentReferencesListRenderer implements IListRenderer { +class ContentReferencesListRenderer implements IListRenderer { static TEMPLATE_ID = 'contentReferencesListRenderer'; readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; @@ -1437,12 +1503,18 @@ class ContentReferencesListRenderer implements IListRenderer 1; + if (isDupe) { + name += ` (${agent.publisherDisplayName})`; + } + + const args: IAgentWidgetArgs = { agentId: agent.id, name, isClickable }; + return `[${agent.name}](${agentRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; +} + +interface IAgentWidgetArgs { + agentId: string; + name: string; + isClickable?: boolean; +} + +export function agentSlashCommandToMarkdown(agent: IChatAgentData, command: IChatAgentCommand): string { + const text = `${chatSubcommandLeader}${command.name}`; + const args: ISlashCommandWidgetArgs = { agentId: agent.id, command: command.name }; + return `[${text}](${agentSlashRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; +} + +interface ISlashCommandWidgetArgs { + agentId: string; + command: string; +} export class ChatMarkdownDecorationsRenderer { constructor( @@ -31,6 +76,9 @@ export class ChatMarkdownDecorationsRenderer { @IChatAgentService private readonly chatAgentService: IChatAgentService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IHoverService private readonly hoverService: IHoverService, + @IChatService private readonly chatService: IChatService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @ICommandService private readonly commandService: ICommandService, ) { } convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string { @@ -39,13 +87,7 @@ export class ChatMarkdownDecorationsRenderer { if (part instanceof ChatRequestTextPart) { result += part.text; } else if (part instanceof ChatRequestAgentPart) { - let text = part.text; - const isDupe = this.chatAgentService.getAgentsByName(part.agent.name).length > 1; - if (isDupe) { - text += ` (${part.agent.publisherDisplayName})`; - } - - result += `[${text}](${agentRefUrl}?${encodeURIComponent(part.agent.id)})`; + result += this.instantiationService.invokeFunction(accessor => agentToMarkdown(part.agent, false, accessor)); } else { const uri = part instanceof ChatRequestDynamicVariablePart && part.data instanceof URI ? part.data : @@ -55,7 +97,7 @@ export class ChatMarkdownDecorationsRenderer { ''; const text = part.text; - result += `[${text}](${variableRefUrl}?${title})`; + result += `[${text}](${decorationRefUrl}?${title})`; } } @@ -68,12 +110,33 @@ export class ChatMarkdownDecorationsRenderer { const href = a.getAttribute('data-href'); if (href) { if (href.startsWith(agentRefUrl)) { - const title = decodeURIComponent(href.slice(agentRefUrl.length + 1)); - a.parentElement!.replaceChild( - this.renderAgentWidget(a.textContent!, title, store), - a); - } else if (href.startsWith(variableRefUrl)) { - const title = decodeURIComponent(href.slice(variableRefUrl.length + 1)); + let args: IAgentWidgetArgs | undefined; + try { + args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1))); + } catch (e) { + this.logService.error('Invalid chat widget render data JSON', toErrorMessage(e)); + } + + if (args) { + a.parentElement!.replaceChild( + this.renderAgentWidget(args, store), + a); + } + } else if (href.startsWith(agentSlashRefUrl)) { + let args: ISlashCommandWidgetArgs | undefined; + try { + args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1))); + } catch (e) { + this.logService.error('Invalid chat slash command render data JSON', toErrorMessage(e)); + } + + if (args) { + a.parentElement!.replaceChild( + this.renderSlashCommandWidget(a.textContent!, args, store), + a); + } + } else if (href.startsWith(decorationRefUrl)) { + const title = decodeURIComponent(href.slice(decorationRefUrl.length + 1)); a.parentElement!.replaceChild( this.renderResourceWidget(a.textContent!, title), a); @@ -88,14 +151,58 @@ export class ChatMarkdownDecorationsRenderer { return store; } - private renderAgentWidget(name: string, id: string, store: DisposableStore): HTMLElement { - const container = dom.$('span.chat-resource-widget', undefined, dom.$('span', undefined, name)); + private renderAgentWidget(args: IAgentWidgetArgs, store: DisposableStore): HTMLElement { + const nameWithLeader = `${chatAgentLeader}${args.name}`; + let container: HTMLElement; + if (args.isClickable) { + container = dom.$('span.chat-agent-widget'); + const button = store.add(new Button(container, { + buttonBackground: asCssVariable(chatSlashCommandBackground), + buttonForeground: asCssVariable(chatSlashCommandForeground), + buttonHoverBackground: undefined + })); + button.label = nameWithLeader; + store.add(button.onDidClick(() => { + const agent = this.chatAgentService.getAgent(args.agentId); + const widget = this.chatWidgetService.lastFocusedWidget; + if (!widget || !agent) { + return; + } + this.chatService.sendRequest(widget.viewModel!.sessionId, agent.metadata.sampleRequest ?? '', { location: widget.location, agentId: agent.id }); + })); + } else { + container = this.renderResourceWidget(nameWithLeader, undefined); + } + + 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, () => { - const hover = store.add(this.instantiationService.createInstance(ChatAgentHover)); - hover.setAgent(id); - return hover.domNode; + hover.value.setAgent(args.agentId); + return hover.value.domNode; + }, agent && getChatAgentHoverOptions(() => agent, this.commandService))); + return container; + } + + private renderSlashCommandWidget(name: string, args: ISlashCommandWidgetArgs, store: DisposableStore): HTMLElement { + const container = dom.$('span.chat-agent-widget.chat-command-widget'); + const agent = this.chatAgentService.getAgent(args.agentId); + const button = store.add(new Button(container, { + buttonBackground: asCssVariable(chatSlashCommandBackground), + buttonForeground: asCssVariable(chatSlashCommandForeground), + buttonHoverBackground: undefined })); + button.label = name; + store.add(button.onDidClick(() => { + const widget = this.chatWidgetService.lastFocusedWidget; + if (!widget || !agent) { + return; + } + + const command = agent.slashCommands.find(c => c.name === args.command); + this.chatService.sendRequest(widget.viewModel!.sessionId, command?.sampleRequest ?? '', { location: widget.location, agentId: agent.id, slashCommand: args.command }); + })); + return container; } @@ -125,10 +232,13 @@ export class ChatMarkdownDecorationsRenderer { } - private renderResourceWidget(name: string, title: string): HTMLElement { + private renderResourceWidget(name: string, title: string | undefined): HTMLElement { const container = dom.$('span.chat-resource-widget'); const alias = dom.$('span', undefined, name); - alias.title = title; + if (title) { + alias.title = title; + } + container.appendChild(alias); return container; } diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts new file mode 100644 index 00000000000..225ecdd120e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownRenderOptions, MarkedOptions } from 'vs/base/browser/markdownRenderer'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownRendererOptions, IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; + +const allowedHtmlTags = [ + 'b', + 'blockquote', + 'br', + 'code', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'li', + 'ol', + 'p', + 'pre', + 'strong', + 'table', + 'tbody', + 'td', + 'th', + 'thead', + 'tr', + 'ul', + 'a', + 'img', + + // Not in the official list, but used for codicons and other vscode markdown extensions + 'span', +]; + +/** + * This wraps the MarkdownRenderer and applies sanitizer options needed for Chat. + */ +export class ChatMarkdownRenderer extends MarkdownRenderer { + constructor( + options: IMarkdownRendererOptions | undefined, + @ILanguageService languageService: ILanguageService, + @IOpenerService openerService: IOpenerService, + @ITrustedDomainService private readonly trustedDomainService: ITrustedDomainService, + ) { + super(options ?? {}, languageService, openerService); + } + + override render(markdown: IMarkdownString | undefined, options?: MarkdownRenderOptions, markedOptions?: MarkedOptions): IMarkdownRenderResult { + options = { + ...options, + remoteImageIsAllowed: (uri) => this.trustedDomainService.isValid(uri), + sanitizerOptions: { + replaceWithPlaintext: true, + allowedTags: allowedHtmlTags, + } + }; + + const mdWithBody: IMarkdownString | undefined = (markdown && markdown.supportHtml) ? + { + ...markdown, + + // dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this. + value: `${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 c50724ba6d1..c89a6a4d712 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { isNonEmptyArray } from 'vs/base/common/arrays'; +import * as strings from 'vs/base/common/strings'; import { Codicon } from 'vs/base/common/codicons'; -import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize, localize2 } from 'vs/nls'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -39,17 +40,18 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi type: 'string' }, name: { - description: localize('chatParticipantName', "User-facing display name for this chat participant. The user will use '@' with this name to invoke the participant."), + description: localize('chatParticipantName', "User-facing name for this chat participant. The user will use '@' with this name to invoke the participant."), + type: 'string', + pattern: '^[\\w0-9_-]+$' + }, + fullName: { + markdownDescription: localize('chatParticipantFullName', "The full name of this chat participant, which is shown as the label for responses coming from this participant. If not provided, {0} is used.", '`name`'), type: 'string' }, description: { description: localize('chatParticipantDescription', "A description of this chat participant, shown in the UI."), type: 'string' }, - isDefault: { - markdownDescription: localize('chatParticipantIsDefaultDescription', "**Only** allowed for extensions that have the `defaultChatParticipant` proposal."), - type: 'boolean', - }, isSticky: { description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), type: 'boolean' @@ -58,13 +60,6 @@ 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' }, - defaultImplicitVariables: { - markdownDescription: '**Only** allowed for extensions that have the `chatParticipantAdditions` proposal. The names of the variables that are invoked by default', - type: 'array', - items: { - type: 'string' - } - }, commands: { markdownDescription: localize('chatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."), type: 'array', @@ -94,26 +89,9 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi description: localize('chatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), type: 'boolean' }, - defaultImplicitVariables: { - markdownDescription: localize('defaultImplicitVariables', "**Only** allowed for extensions that have the `chatParticipantAdditions` proposal. The names of the variables that are invoked by default"), - type: 'array', - items: { - type: 'string' - } - }, } } }, - locations: { - markdownDescription: localize('chatLocationsDescription', "Locations in which this chat participant is available."), - type: 'array', - default: ['panel'], - items: { - type: 'string', - enum: ['panel', 'terminal', 'notebook'] - } - - } } } }, @@ -181,13 +159,34 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { private handleAndRegisterChatExtensions(): void { chatParticipantExtensionPoint.setHandler((extensions, delta) => { for (const extension of delta.added) { + if (this.productService.quality === 'stable' && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + this.logService.warn(`Chat participants are not yet enabled in VS Code Stable (${extension.description.identifier.value})`); + 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_-]+$/.`); + continue; + } + + if (providerDescriptor.fullName && strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter(providerDescriptor.fullName)) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains ambiguous characters: ${providerDescriptor.fullName}.`); + continue; + } + + // Spaces are allowed but considered "invisible" + if (providerDescriptor.fullName && strings.InvisibleCharacters.containsInvisibleCharacter(providerDescriptor.fullName.replace(/ /g, ''))) { + this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains invisible characters: ${providerDescriptor.fullName}.`); + continue; + } + if (providerDescriptor.isDefault && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`); continue; } - if (providerDescriptor.defaultImplicitVariables && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { + if ((providerDescriptor.defaultImplicitVariables || providerDescriptor.locations) && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`); continue; } @@ -216,6 +215,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { sampleRequest: providerDescriptor.sampleRequest, }, name: providerDescriptor.name, + fullName: providerDescriptor.fullName, isDefault: providerDescriptor.isDefault, defaultImplicitVariables: providerDescriptor.defaultImplicitVariables, locations: isNonEmptyArray(providerDescriptor.locations) ? @@ -257,21 +257,30 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { return viewContainer; } + private hasRegisteredDefaultParticipantView = false; private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable { + if (this.hasRegisteredDefaultParticipantView) { + this.logService.warn(`Tried to register a second default chat participant view for "${defaultParticipantDescriptor.id}"`); + return Disposable.None; + } + // Register View + const name = defaultParticipantDescriptor.fullName ?? defaultParticipantDescriptor.name; const viewDescriptor: IViewDescriptor[] = [{ id: CHAT_VIEW_ID, containerIcon: this._viewContainer.icon, containerTitle: this._viewContainer.title.value, singleViewPaneContainerTitle: this._viewContainer.title.value, - name: { value: defaultParticipantDescriptor.name, original: defaultParticipantDescriptor.name }, + name: { value: name, original: name }, canToggleVisibility: false, canMoveView: true, ctorDescriptor: new SyncDescriptor(ChatViewPane), }]; + this.hasRegisteredDefaultParticipantView = true; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); return toDisposable(() => { + this.hasRegisteredDefaultParticipantView = false; Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index d52ce247794..f40ad589c60 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -206,14 +206,15 @@ class QuickChat extends Disposable { render(parent: HTMLElement): void { if (this.widget) { + // NOTE: if this changes, we need to make sure disposables in this function are tracked differently. throw new Error('Cannot render quick chat twice'); } - const scopedInstantiationService = this.instantiationService.createChild( + const scopedInstantiationService = this._register(this.instantiationService.createChild( new ServiceCollection([ IContextKeyService, this._register(this.contextKeyService.createScoped(parent)) ]) - ); + )); this.widget = this._register( scopedInstantiationService.createInstance( ChatWidget, @@ -271,7 +272,7 @@ class QuickChat extends Disposable { })); } - async acceptInput(): Promise { + async acceptInput() { return this.widget.acceptInput(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts new file mode 100644 index 00000000000..d97ecba4f42 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.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 { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; +import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { alertAccessibleViewFocusChange, IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IChatWidgetService, IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; + +export class ChatResponseAccessibleView implements IAccessibleViewImplentation { + readonly priority = 100; + readonly name = 'panelChat'; + readonly type = AccessibleViewType.View; + readonly when = CONTEXT_IN_CHAT_SESSION; + getProvider(accessor: ServicesAccessor) { + const widgetService = accessor.get(IChatWidgetService); + const codeEditorService = accessor.get(ICodeEditorService); + return resolveProvider(widgetService, codeEditorService, true); + function resolveProvider(widgetService: IChatWidgetService, codeEditorService: ICodeEditorService, initialRender?: boolean) { + const widget = widgetService.lastFocusedWidget; + if (!widget) { + return; + } + const chatInputFocused = initialRender && !!codeEditorService.getFocusedCodeEditor(); + if (initialRender && chatInputFocused) { + widget.focusLastMessage(); + } + + if (!widget) { + return; + } + + const verifiedWidget: IChatWidget = widget; + const focusedItem = verifiedWidget.getFocus(); + + if (!focusedItem) { + return; + } + + widget.focus(focusedItem); + const isWelcome = focusedItem instanceof ChatWelcomeMessageModel; + let responseContent = isResponseVM(focusedItem) ? focusedItem.response.asString() : undefined; + if (isWelcome) { + const welcomeReplyContents = []; + for (const content of focusedItem.content) { + if (Array.isArray(content)) { + welcomeReplyContents.push(...content.map(m => m.message)); + } else { + welcomeReplyContents.push((content as IMarkdownString).value); + } + } + responseContent = welcomeReplyContents.join('\n'); + } + if (!responseContent && 'errorDetails' in focusedItem && focusedItem.errorDetails) { + responseContent = focusedItem.errorDetails.message; + } + if (!responseContent) { + return; + } + const responses = verifiedWidget.viewModel?.getItems().filter(i => isResponseVM(i)); + const length = responses?.length; + const responseIndex = responses?.findIndex(i => i === focusedItem); + + return { + id: AccessibleViewProviderId.Chat, + verbositySettingKey: AccessibilityVerbositySettingId.Chat, + provideContent(): string { return renderMarkdownAsPlaintext(new MarkdownString(responseContent), true); }, + onClose() { + verifiedWidget.reveal(focusedItem); + if (chatInputFocused) { + verifiedWidget.focusInput(); + } else { + verifiedWidget.focus(focusedItem); + } + }, + next() { + verifiedWidget.moveFocus(focusedItem, 'next'); + alertAccessibleViewFocusChange(responseIndex, length, 'next'); + resolveProvider(widgetService, codeEditorService); + }, + previous() { + verifiedWidget.moveFocus(focusedItem, 'previous'); + alertAccessibleViewFocusChange(responseIndex, length, 'previous'); + resolveProvider(widgetService, codeEditorService); + }, + options: { type: AccessibleViewType.View } + }; + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 3051205846d..acde7f02757 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -3,17 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { basename } from 'vs/base/common/path'; import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { Location } from 'vs/editor/common/languages'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatDynamicVariableModel } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestDynamicVariablePart, ChatRequestVariablePart, IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatContentReference } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ChatContextAttachments } from 'vs/workbench/contrib/chat/browser/contrib/chatContextAttachments'; interface IChatData { data: IChatVariableData; @@ -30,7 +35,7 @@ export class ChatVariablesService implements IChatVariablesService { ) { } - async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { let resolvedVariables: IChatRequestVariableEntry[] = []; const jobs: Promise[] = []; @@ -49,12 +54,35 @@ export class ChatVariablesService implements IChatVariablesService { }; jobs.push(data.resolver(prompt.text, part.variableArg, model, variableProgressCallback, token).then(value => { if (value) { - resolvedVariables[i] = { name: part.variableName, range: part.range, value, references }; + resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: part.variableName, range: part.range, value, references }; } }).catch(onUnexpectedExternalError)); } } else if (part instanceof ChatRequestDynamicVariablePart) { - resolvedVariables[i] = { name: part.referenceText, range: part.range, value: part.data }; + resolvedVariables[i] = { id: part.id, name: part.referenceText, range: part.range, value: part.data }; + } + }); + + const resolvedAttachedContext: IChatRequestVariableEntry[] = []; + attachedContextVariables + ?.forEach((attachment, i) => { + const data = this._resolver.get(attachment.name?.toLowerCase()); + if (data) { + const references: IChatContentReference[] = []; + const variableProgressCallback = (item: IChatVariableResolverProgress) => { + if (item.kind === 'reference') { + references.push(item); + return; + } + progress(item); + }; + jobs.push(data.resolver(prompt.text, '', model, variableProgressCallback, token).then(value => { + if (value) { + resolvedAttachedContext[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, range: attachment.range, value, references }; + } + }).catch(onUnexpectedExternalError)); + } else if (attachment.isDynamic) { + resolvedAttachedContext[i] = { id: attachment.id, name: attachment.name, value: attachment.value }; } }); @@ -64,6 +92,7 @@ export class ChatVariablesService implements IChatVariablesService { // "reverse", high index first so that replacement is simple resolvedVariables.sort((a, b) => b.range!.start - a.range!.start); + resolvedVariables.push(...resolvedAttachedContext); return { variables: resolvedVariables, @@ -120,4 +149,30 @@ export class ChatVariablesService implements IChatVariablesService { this._resolver.delete(key); }); } + + async attachContext(name: string, value: string | URI | Location, location: ChatAgentLocation) { + if (location !== ChatAgentLocation.Panel) { + return; + } + + const widget = this.chatWidgetService.lastFocusedWidget; + if (!widget || !widget.viewModel) { + return; + } + + const key = name.toLowerCase(); + if (key === 'file' && typeof value !== 'string') { + const uri = URI.isUri(value) ? value : value.uri; + const range = 'range' in value ? value.range : undefined; + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { value, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri.path), isDynamic: true }); + return; + } + + const resolved = this._resolver.get(key); + if (!resolved) { + return; + } + + widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { ...resolved.data, value }); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 5a8b65c964e..6b19f886cf1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -33,8 +33,8 @@ import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from 'v import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatModelInitState, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, extractAgentAndCommand } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +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 { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -96,6 +96,12 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidAcceptInput = this._register(new Emitter()); readonly onDidAcceptInput = this._onDidAcceptInput.event; + private _onDidDeleteContext = this._register(new Emitter()); + readonly onDidDeleteContext = this._onDidDeleteContext.event; + + private _onDidHide = this._register(new Emitter()); + readonly onDidHide = this._onDidHide.event; + private _onDidChangeParsedInput = this._register(new Emitter()); readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event; @@ -161,6 +167,10 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.parsedChatRequest; } + get scopedContextKeyService(): IContextKeyService { + return this.contextKeyService; + } + constructor( readonly location: ChatAgentLocation, readonly viewContext: IChatWidgetViewContext, @@ -384,9 +394,11 @@ export class ChatWidget extends Disposable implements IChatWidget { } setVisible(visible: boolean): void { + const wasVisible = this._visible; this._visible = visible; this.visibleChangeCount++; this.renderer.setVisible(visible); + this.input.setVisible(visible); if (visible) { this._register(disposableTimeout(() => { @@ -396,11 +408,13 @@ export class ChatWidget extends Disposable implements IChatWidget { this.onDidChangeItems(true); } }, 0)); + } else if (wasVisible) { + this._onDidHide.fire(); } } private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void { - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); + const scopedInstantiationService = 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, @@ -425,6 +439,12 @@ export class ChatWidget extends Disposable implements IChatWidget { // is this used anymore? this.acceptInput(item.message); })); + this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(item => { + const request = this.chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); + if (request) { + this.chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt, location: this.location }).catch(e => this.logService.error('FAILED to rerun request', e)); + } + })); this.tree = >scopedInstantiationService.createInstance( WorkbenchObjectTree, @@ -527,6 +547,7 @@ export class ChatWidget extends Disposable implements IChatWidget { }); })); this._register(this.inputPart.onDidFocus(() => this._onDidFocus.fire())); + this._register(this.inputPart.onDidDeleteContext((e) => this._onDidDeleteContext.fire(e))); this._register(this.inputPart.onDidAcceptFollowup(e => { if (!this.viewModel) { return; @@ -574,12 +595,8 @@ export class ChatWidget extends Disposable implements IChatWidget { } this._onDidChangeContentHeight.fire(); })); - this._register(this.inputEditor.onDidChangeModelContent(() => this.updateImplicitContextKinds())); - this._register(this.chatAgentService.onDidChangeAgents(() => { - if (this.viewModel) { - this.updateImplicitContextKinds(); - } - })); + this._register(this.inputEditor.onDidChangeModelContent(() => this.parsedChatRequest = undefined)); + this._register(this.chatAgentService.onDidChangeAgents(() => this.parsedChatRequest = undefined)); } private onDidStyleChange(): void { @@ -588,23 +605,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); } - private updateImplicitContextKinds() { - if (!this.viewModel) { - return; - } - this.parsedChatRequest = undefined; - const agentAndSubcommand = extractAgentAndCommand(this.parsedInput); - const currentAgent = agentAndSubcommand.agentPart?.agent ?? this.chatAgentService.getDefaultAgent(this.location); - const implicitVariables = agentAndSubcommand.commandPart ? - agentAndSubcommand.commandPart.command.defaultImplicitVariables : - currentAgent?.defaultImplicitVariables; - this.inputPart.setImplicitContextKinds(implicitVariables ?? []); - - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } - } - setModel(model: IChatModel, viewState: IChatViewState): void { if (!this.container) { throw new Error('Call render() before setModel()'); @@ -647,7 +647,6 @@ export class ChatWidget extends Disposable implements IChatWidget { revealLastElement(this.tree); } - this.updateImplicitContextKinds(); } getFocus(): ChatTreeItem | undefined { @@ -689,8 +688,8 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.inputPart.inputEditor.getValue(); } - async acceptInput(query?: string): Promise { - this._acceptInput(query ? { query } : undefined); + async acceptInput(query?: string): Promise { + return this._acceptInput(query ? { query } : undefined); } async acceptInputWithPrefix(prefix: string): Promise { @@ -707,7 +706,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return inputState; } - private async _acceptInput(opts: { query: string } | { prefix: string } | undefined): Promise { + private async _acceptInput(opts: { query: string } | { prefix: string } | undefined): Promise { if (this.viewModel) { this._onDidAcceptInput.fire(); @@ -717,19 +716,34 @@ 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, { implicitVariablesEnabled: this.inputPart.implicitContextEnabled, location: this.location, parserContext: { selectedAgent: this._lastSelectedAgent } }); + const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { location: this.location, 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._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); - result.responseCompletePromise.then(async () => { + result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; this.chatAccessibilityService.acceptResponse(lastResponse, requestId); }); + return result.responseCreatedPromise; } } + return undefined; + } + + + setContext(overwrite: boolean, ...contentReferences: IChatRequestVariableEntry[]) { + if (overwrite) { + this.inputPart.attachedContext.clear(); + } + this.inputPart.attachContext(...contentReferences); + + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); + } } getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts new file mode 100644 index 00000000000..ff0a4455486 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatContextAttachments.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatWidget, IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; + +export class ChatContextAttachments extends Disposable implements IChatWidgetContrib { + + private _attachedContext = new Set(); + + public static readonly ID = 'chatContextAttachments'; + + get id() { + return ChatContextAttachments.ID; + } + + constructor(readonly widget: IChatWidget) { + super(); + + this._register(this.widget.onDidDeleteContext((e) => { + this._removeContext(e); + })); + + this._register(this.widget.onDidSubmitAgent(() => { + this._clearAttachedContext(); + })); + } + + getInputState?() { + return [...this._attachedContext.values()]; + } + + setInputState?(s: any): void { + if (!Array.isArray(s)) { + return; + } + + this.widget.setContext(true, ...s); + } + + getContext() { + return new Set([...this._attachedContext.values()].map((v) => v.id)); + } + + setContext(overwrite: boolean, ...attachments: IChatRequestVariableEntry[]) { + if (overwrite) { + this._attachedContext.clear(); + } + for (const attachment of attachments) { + this._attachedContext.add(attachment); + } + + this.widget.setContext(overwrite, ...attachments); + } + + private _removeContext(attachment: IChatRequestVariableEntry) { + this._attachedContext.delete(attachment); + } + + private _clearAttachedContext() { + this._attachedContext.clear(); + } +} + +ChatWidget.CONTRIBS.push(ChatContextAttachments); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 12163ceaf03..4c3fa0ee29c 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -127,6 +127,11 @@ function isSelectAndInsertFileActionContext(context: any): context is SelectAndI } export class SelectAndInsertFileAction extends Action2 { + static readonly Name = 'files'; + static readonly Item = { + label: localize('allFiles', 'All Files'), + description: localize('allFilesDescription', 'Search for relevant files in the workspace and provide context from them'), + }; static readonly ID = 'workbench.action.chat.selectAndInsertFile'; constructor() { @@ -153,18 +158,13 @@ export class SelectAndInsertFileAction extends Action2 { }; let options: IQuickAccessOptions | undefined; - const filesVariableName = 'files'; - const filesItem = { - label: localize('allFiles', 'All Files'), - description: localize('allFilesDescription', 'Search for relevant files in the workspace and provide context from them'), - }; // If we have a `files` variable, add an option to select all files in the picker. // This of course assumes that the `files` variable has the behavior that it searches // through files in the workspace. - if (chatVariablesService.hasVariable(filesVariableName)) { + if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { options = { providerOptions: { - additionPicks: [filesItem, { type: 'separator' }] + additionPicks: [SelectAndInsertFileAction.Item, { type: 'separator' }] }, }; } @@ -180,8 +180,8 @@ export class SelectAndInsertFileAction extends Action2 { const range = context.range; // Handle the special case of selecting all files - if (picks[0] === filesItem) { - const text = `#${filesVariableName}`; + if (picks[0] === SelectAndInsertFileAction.Item) { + const text = `#${SelectAndInsertFileAction.Name}`; const success = editor.executeEdits('chatInsertFile', [{ range, text: text + ' ' }]); if (!success) { logService.trace(`SelectAndInsertFileAction: failed to insert "${text}"`); @@ -208,6 +208,7 @@ export class SelectAndInsertFileAction extends Action2 { } context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ + id: 'vscode.file', range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, data: resource }); @@ -216,6 +217,7 @@ export class SelectAndInsertFileAction extends Action2 { registerAction2(SelectAndInsertFileAction); export interface IAddDynamicVariableContext { + id: string; widget: IChatWidget; range: IRange; variableData: IChatRequestVariableValue; @@ -275,6 +277,7 @@ export class AddDynamicVariableAction extends Action2 { } context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ + id: context.id, range: range, data: variableData }); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts deleted file mode 100644 index 1d8dfd377d8..00000000000 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables.ts +++ /dev/null @@ -1,34 +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 { Disposable } from 'vs/base/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; - -class ChatHistoryVariables extends Disposable { - constructor( - @IChatVariablesService chatVariablesService: IChatVariablesService, - ) { - super(); - - this._register(chatVariablesService.registerVariable({ name: 'response', description: '', canTakeArgument: true, hidden: true }, async (message, arg, model, progress, token) => { - if (!arg) { - return undefined; - } - - const responseNum = parseInt(arg, 10); - const response = model.getRequests()[responseNum - 1].response; - if (!response) { - return undefined; - } - - return response.response.asString(); - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ChatHistoryVariables, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts new file mode 100644 index 00000000000..c94042618c9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -0,0 +1,401 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from 'vs/base/common/lifecycle'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper'; +import { CompletionContext, CompletionItem, CompletionItemKind } 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'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +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'; +import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; +import { SelectAndInsertFileAction } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { ChatAgentLocation, getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +class SlashCommandCompletions extends Disposable { + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'globalSlashCommands', + 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*/) { + return null; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); + if (usedAgent) { + // No (classic) global slash commands when an agent is used + return; + } + + const slashCommands = this.chatSlashCommandService.getCommands(); + if (!slashCommands) { + return null; + } + + return { + suggestions: slashCommands.map((c, i): CompletionItem => { + const withSlash = `/${c.command}`; + return { + label: withSlash, + insertText: c.executeImmediately ? '' : `${withSlash} `, + detail: c.detail, + range: new Range(1, 1, 1, 1), + sortText: c.sortText ?? 'a'.repeat(i + 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway, + command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, + }; + }) + }; + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); + +class AgentCompletions extends Disposable { + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgent', + 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*/) { + return null; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); + if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { + // Only one agent allowed + return; + } + + const range = computeCompletionRanges(model, position, /@\w*/g); + if (!range) { + return null; + } + + const agents = this.chatAgentService.getAgents() + .filter(a => !a.isDefault) + .filter(a => a.locations.includes(widget.location)); + + return { + suggestions: agents.map((agent, i): CompletionItem => { + const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); + return { + // Leading space is important because detail has no space at the start by design + label: isDupe ? + { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : + agentLabel, + insertText: `${agentLabel} `, + detail: agent.description, + range: new Range(1, 1, 1, 1), + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: agent, widget } satisfies AssignSelectedAgentActionArgs] }, + kind: CompletionItemKind.Text, // The icons are disabled here anyway + }; + }) + }; + } + })); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgentSubcommand', + 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*/) { + return; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + const parsedRequest = widget.parsedInput.parts; + const usedAgentIdx = parsedRequest.findIndex((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + if (usedAgentIdx < 0) { + return; + } + + const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); + if (usedSubcommand) { + // Only one allowed + return; + } + + for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) { + // Could allow text after 'position' + if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/\w*)?$/)) { + // No text allowed between agent and subcommand + return; + } + } + + const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; + return { + suggestions: usedAgent.agent.slashCommands.map((c, i): CompletionItem => { + const withSlash = `/${c.name}`; + return { + label: withSlash, + insertText: `${withSlash} `, + detail: c.description, + range, + kind: CompletionItemKind.Text, // The icons are disabled here anyway + }; + }) + }; + } + })); + + // list subcommands when the query is empty, insert agent+subcommand + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatAgentAndSubcommand', + triggerCharacters: ['/'], + 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*/) { + return; + } + + const range = computeCompletionRanges(model, position, /\/\w*/g); + if (!range) { + return null; + } + + const agents = this.chatAgentService.getAgents() + .filter(a => a.locations.includes(widget.location)); + + const justAgents: CompletionItem[] = agents + .filter(a => !a.isDefault) + .map(agent => { + const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); + const detail = agent.description; + + return { + label: isDupe ? + { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : + agentLabel, + detail, + filterText: `${chatSubcommandLeader}${agent.name}`, + insertText: `${agentLabel} `, + range: new Range(1, 1, 1, 1), + kind: CompletionItemKind.Text, + sortText: `${chatSubcommandLeader}${agent.id}`, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, + }; + }); + + return { + suggestions: justAgents.concat( + agents.flatMap(agent => agent.slashCommands.map((c, i) => { + const { label: agentLabel, isDupe } = getAgentCompletionDetails(agent, agents, this.chatAgentNameService); + const withSlash = `${chatSubcommandLeader}${c.name}`; + return { + label: { label: withSlash, description: agentLabel, detail: isDupe ? ` (${agent.publisherDisplayName})` : undefined }, + filterText: `${chatSubcommandLeader}${agent.name}${c.name}`, + commitCharacters: [' '], + insertText: `${agentLabel} ${withSlash} `, + detail: `(${agentLabel}) ${c.description ?? ''}`, + range: new Range(1, 1, 1, 1), + kind: CompletionItemKind.Text, // The icons are disabled here anyway + sortText: `${chatSubcommandLeader}${agent.id}${c.name}`, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, + } satisfies CompletionItem; + }))) + }; + } + })); + } +} +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); + +interface AssignSelectedAgentActionArgs { + agent: IChatAgentData; + widget: IChatWidget; +} + +class AssignSelectedAgentAction extends Action2 { + static readonly ID = 'workbench.action.chat.assignSelectedAgent'; + + constructor() { + super({ + id: AssignSelectedAgentAction.ID, + title: '' // not displayed + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const arg: AssignSelectedAgentActionArgs = args[0]; + if (!arg || !arg.widget || !arg.agent) { + return; + } + + arg.widget.lastSelectedAgent = arg.agent; + } +} +registerAction2(AssignSelectedAgentAction); + +class BuiltinDynamicCompletions extends Disposable { + private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag + + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatDynamicCompletions', + 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*/) { + return null; + } + + const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef); + if (!range) { + return null; + } + + const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); + return { + suggestions: [ + { + label: `${chatVariableLeader}file`, + insertText: `${chatVariableLeader}file:`, + detail: localize('pickFileLabel', "Pick a file"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, + sortText: 'z' + } satisfies CompletionItem + ] + }; + } + })); + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BuiltinDynamicCompletions, LifecyclePhase.Eventually); + +function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range; varWord: IWordAtPosition | null } | undefined { + const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); + if (!varWord && model.getWordUntilPosition(position).word) { + // inside a "normal" word + return; + } + + let insert: Range; + let replace: Range; + if (!varWord) { + insert = replace = Range.fromPositions(position); + } else { + insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); + replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); + } + + return { insert, replace, varWord }; +} + +class VariableCompletions extends Disposable { + + private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag + + constructor( + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, + ) { + super(); + + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + _debugDisplayName: 'chatVariables', + triggerCharacters: [chatVariableLeader], + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + + const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { + return null; + } + + const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef); + if (!range) { + return null; + } + + const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); + const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; + + const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); + const variableItems = Array.from(this.chatVariablesService.getVariables()) + // This doesn't look at dynamic variables like `file`, where multiple makes sense. + .filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name)) + .filter(v => !v.isSlow || slowSupported) + .map((v): CompletionItem => { + const withLeader = `${chatVariableLeader}${v.name}`; + return { + label: withLeader, + range, + insertText: withLeader + ' ', + detail: v.description, + kind: CompletionItemKind.Text, // The icons are disabled here anyway + sortText: 'z' + }; + }); + + return { + suggestions: variableItems + }; + } + })); + } +} + +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 30fd0e2f170..bafc22d77ee 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -3,36 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IWordAtPosition, getWordAtText } from 'vs/editor/common/core/wordHelper'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; -import { CompletionContext, CompletionItem, CompletionItemKind } 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'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { Registry } from 'vs/platform/registry/common/platform'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; -import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; +import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import { SelectAndInsertFileAction, dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; +import { dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; import { ChatAgentLocation, 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, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +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'; -import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; const decorationDescription = 'chat'; const placeholderDecorationType = 'chat-session-detail'; @@ -266,369 +251,6 @@ class InputEditorSlashCommandMode extends Disposable { ChatWidget.CONTRIBS.push(InputEditorDecorations, InputEditorSlashCommandMode); -class SlashCommandCompletions extends Disposable { - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'globalSlashCommands', - 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*/) { - return null; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); - if (usedAgent) { - // No (classic) global slash commands when an agent is used - return; - } - - const slashCommands = this.chatSlashCommandService.getCommands(); - if (!slashCommands) { - return null; - } - - return { - suggestions: slashCommands.map((c, i): CompletionItem => { - const withSlash = `/${c.command}`; - return { - label: withSlash, - insertText: c.executeImmediately ? '' : `${withSlash} `, - detail: c.detail, - range: new Range(1, 1, 1, 1), - sortText: c.sortText ?? 'a'.repeat(i + 1), - kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, - }; - }) - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); - -class AgentCompletions extends Disposable { - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgent', - 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*/) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); - if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { - // Only one agent allowed - return; - } - - const range = computeCompletionRanges(model, position, /@\w*/g); - if (!range) { - return null; - } - - const agents = this.chatAgentService.getAgents() - .filter(a => !a.isDefault) - .filter(a => a.locations.includes(widget.location)); - - return { - suggestions: agents.map((a, i): CompletionItem => { - const withAt = `@${a.name}`; - const isDupe = !!agents.find(other => other.name === a.name && other.id !== a.id); - return { - // Leading space is important because detail has no space at the start by design - label: isDupe ? - { label: withAt, description: a.description, detail: ` (${a.publisherDisplayName})` } : - withAt, - insertText: `${withAt} `, - detail: a.description, - range: new Range(1, 1, 1, 1), - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: a, widget } satisfies AssignSelectedAgentActionArgs] }, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - }; - }) - }; - } - })); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgentSubcommand', - 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*/) { - return; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgentIdx = parsedRequest.findIndex((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); - if (usedAgentIdx < 0) { - return; - } - - const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); - if (usedSubcommand) { - // Only one allowed - return; - } - - for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) { - // Could allow text after 'position' - if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/\w*)?$/)) { - // No text allowed between agent and subcommand - return; - } - } - - const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; - return { - suggestions: usedAgent.agent.slashCommands.map((c, i): CompletionItem => { - const withSlash = `/${c.name}`; - return { - label: withSlash, - insertText: `${withSlash} `, - detail: c.description, - range, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - }; - }) - }; - } - })); - - // list subcommands when the query is empty, insert agent+subcommand - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgentAndSubcommand', - triggerCharacters: ['/'], - 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*/) { - return; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const agents = this.chatAgentService.getAgents() - .filter(a => a.locations.includes(widget.location)); - - const justAgents: CompletionItem[] = agents - .filter(a => !a.isDefault) - .map(agent => { - const isDupe = !!agents.find(other => other.name === agent.name && other.id !== agent.id); - const detail = agent.description; - const agentLabel = `${chatAgentLeader}${agent.name}`; - - return { - label: isDupe ? - { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : - agentLabel, - detail, - filterText: `${chatSubcommandLeader}${agent.name}`, - insertText: `${agentLabel} `, - range: new Range(1, 1, 1, 1), - kind: CompletionItemKind.Text, - sortText: `${chatSubcommandLeader}${agent.id}`, - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, - }; - }); - - return { - suggestions: justAgents.concat( - agents.flatMap(agent => agent.slashCommands.map((c, i) => { - const agentLabel = `${chatAgentLeader}${agent.name}`; - const withSlash = `${chatSubcommandLeader}${c.name}`; - return { - label: { label: withSlash, description: agentLabel }, - filterText: `${chatSubcommandLeader}${agent.name}${c.name}`, - commitCharacters: [' '], - insertText: `${agentLabel} ${withSlash} `, - detail: `(${agentLabel}) ${c.description ?? ''}`, - range: new Range(1, 1, 1, 1), - kind: CompletionItemKind.Text, // The icons are disabled here anyway - sortText: `${chatSubcommandLeader}${agent.id}${c.name}`, - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, - } satisfies CompletionItem; - }))) - }; - } - })); - } -} -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); - -interface AssignSelectedAgentActionArgs { - agent: IChatAgentData; - widget: IChatWidget; -} - -class AssignSelectedAgentAction extends Action2 { - static readonly ID = 'workbench.action.chat.assignSelectedAgent'; - - constructor() { - super({ - id: AssignSelectedAgentAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const arg: AssignSelectedAgentActionArgs = args[0]; - if (!arg || !arg.widget || !arg.agent) { - return; - } - - arg.widget.lastSelectedAgent = arg.agent; - } -} -registerAction2(AssignSelectedAgentAction); - -class BuiltinDynamicCompletions extends Disposable { - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatDynamicCompletions', - 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*/) { - return null; - } - - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef); - if (!range) { - return null; - } - - const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); - return { - suggestions: [ - { - label: `${chatVariableLeader}file`, - insertText: `${chatVariableLeader}file:`, - detail: localize('pickFileLabel', "Pick a file"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, - sortText: 'z' - } satisfies CompletionItem - ] - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BuiltinDynamicCompletions, LifecyclePhase.Eventually); - -function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range; varWord: IWordAtPosition | null } | undefined { - const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); - if (!varWord && model.getWordUntilPosition(position).word) { - // inside a "normal" word - return; - } - - let insert: Range; - let replace: Range; - if (!varWord) { - insert = replace = Range.fromPositions(position); - } else { - insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); - replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); - } - - return { insert, replace, varWord }; -} - -class VariableCompletions extends Disposable { - - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatVariables', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || widget.location !== ChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef); - if (!range) { - return null; - } - - const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); - const variableItems = Array.from(this.chatVariablesService.getVariables()) - // This doesn't look at dynamic variables like `file`, where multiple makes sense. - .filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name)) - .map((v): CompletionItem => { - const withLeader = `${chatVariableLeader}${v.name}`; - return { - label: withLeader, - range, - insertText: withLeader + ' ', - detail: v.description, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - sortText: 'z' - }; - }); - - return { - suggestions: variableItems - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); - class ChatTokenDeleter extends Disposable { public readonly id = 'chatTokenDeleter'; diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 55f4356b0af..5310cc02f75 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -39,18 +39,23 @@ .interactive-item-container .header .user { display: flex; align-items: center; - gap: 6px; + gap: 8px; } .interactive-item-container .header .username { margin: 0; - font-size: 12px; + font-size: 13px; font-weight: 600; } .interactive-item-container .header .detail-container { - font-size: 0.9em; - opacity: 0.7; + font-size: 12px; + color: var(--vscode-descriptionForeground); +} + +.interactive-item-container .header .detail-container .detail .agentOrSlashCommandDetected A { + cursor: pointer; + color: var(--vscode-textLink-foreground); } .interactive-item-container .chat-animated-ellipsis { @@ -124,23 +129,6 @@ font-size: 14px; } -.interactive-item-container .header .agent-avatar-container { - margin-left: -30px; - transition: margin 0.15s ease-out; - transition-delay: 0.5s; - z-index: -1; -} - -.interactive-item-container .header .agent-avatar-container.loading { - margin-left: 0px; - z-index: 1; -} - -.interactive-item-container .header .agent-avatar-container.complete { - margin-left: -12px; - z-index: 1; -} - .monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar, .monaco-list:not(:focus-within) .monaco-list-row .interactive-item-container:not(:hover) .header .monaco-toolbar, .monaco-list-row:not(.focused) .interactive-item-container:not(:hover) .header .monaco-toolbar .action-label, @@ -182,6 +170,10 @@ width: 100%; } +.interactive-item-container .chat-progress-task { + padding-bottom: 8px; +} + .interactive-item-container .value .rendered-markdown table { width: 100%; text-align: left; @@ -219,7 +211,6 @@ .interactive-request { border-bottom: 1px solid var(--vscode-chat-requestBorder); border-top: 1px solid var(--vscode-chat-requestBorder); - background-color: var(--vscode-chat-requestBackground); } .hc-black .interactive-request, @@ -230,7 +221,7 @@ .interactive-item-container .value { white-space: normal; - word-wrap: break-word; + overflow-wrap: anywhere; } .interactive-item-container .value > :last-child.rendered-markdown > :last-child { @@ -257,10 +248,17 @@ } .interactive-item-container .value .rendered-markdown p { - margin: 0 0 16px 0; line-height: 1.5em; } +.interactive-item-container .value > .rendered-markdown p { + margin: 0 0 16px 0; +} + +.interactive-item-container .value > .rendered-markdown li > p { + margin: 0; +} + .interactive-item-container .value .rendered-markdown ul { padding-inline-start: 24px; } @@ -273,6 +271,10 @@ line-height: 1.3rem; } +.interactive-item-container .value .rendered-markdown img { + max-width: 100%; +} + .interactive-item-container .monaco-tokenized-source, .interactive-item-container code { font-family: var(--monaco-monospace-font); @@ -318,10 +320,14 @@ min-height: 0; } -.interactive-item-container.interactive-item-compact .value .rendered-markdown p { +.interactive-item-container.interactive-item-compact .value > .rendered-markdown p { margin: 0 0 8px 0; } +.interactive-item-container.interactive-item-compact .value > .rendered-markdown li > p { + margin: 0; +} + .interactive-item-container.interactive-item-compact .value .rendered-markdown h1 { margin: 8px 0; @@ -335,10 +341,6 @@ margin: 8px 0; } -.interactive-item-container.interactive-item-compact .value .rendered-markdown p { - margin: 0 0 8px 0; -} - .interactive-session .interactive-input-and-execute-toolbar { display: flex; box-sizing: border-box; @@ -355,6 +357,7 @@ .interactive-session .interactive-input-part.compact .interactive-input-and-execute-toolbar { margin-bottom: 0; + border-radius: 2px; } .interactive-session .interactive-input-and-side-toolbar { @@ -413,10 +416,9 @@ .chat-notification-widget .chat-info-codicon, .chat-notification-widget .chat-error-codicon, .chat-notification-widget .chat-warning-codicon { - margin-left: 3px; display: flex; align-items: start; - gap: 6px; + gap: 8px; } .interactive-item-container .value .chat-notification-widget .rendered-markdown p { @@ -443,6 +445,14 @@ margin-top: 1px; } +.chat-used-context-list .codicon-warning { + color: var(--vscode-notificationsWarningIcon-foreground); /* Have to override default styles which apply to all lists */ +} + +.chat-used-context-list .monaco-icon-label-container { + color: var(--vscode-interactive-session-foreground); +} + .chat-notification-widget .chat-warning-codicon .codicon-warning { color: var(--vscode-notificationsWarningIcon-foreground) !important; /* Have to override default styles which apply to all lists */ } @@ -466,14 +476,54 @@ .interactive-session .interactive-input-part.compact { margin: 0; - padding: 6px 0px; + padding: 8px 0 0 0 } -.interactive-session .chat-implicit-context { - padding: 8px 8px 13px; - margin-bottom: -5px; - border: 1px solid var(--vscode-input-border, var(--vscode-input-background, transparent)); - border-radius: 6px 6px 0px 0px; +.interactive-session .chat-attached-context .chat-attached-context-attachment { + display: flex; + gap: 4px; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover { + cursor: pointer; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button { + display: flex; + align-items: center; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container { + display: flex; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container .monaco-highlighted-label { + display: flex !important; + align-items: center !important; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .monaco-button.codicon.codicon-close, +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button.codicon.codicon-close { + color: var(--vscode-descriptionForeground); +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .codicon { + padding-left: 4px; +} + +.interactive-session .chat-attached-context { + padding: 0 0 8px 0; + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment { + padding: 2px; + border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); + border-radius: 4px; + height: 18px; + max-width: 100%; } .interactive-session-followups { @@ -605,11 +655,19 @@ .interactive-item-container .chat-resource-widget { background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); +} + +.interactive-item-container .chat-resource-widget, +.interactive-item-container .chat-agent-widget .monaco-button { border-radius: 4px; - white-space: nowrap; padding: 1px 3px; } +.interactive-item-container .chat-agent-widget .monaco-text-button { + display: inline; + border: none; +} + .interactive-session .chat-used-context.chat-used-context-collapsed .chat-used-context-list { display: none; } @@ -617,7 +675,7 @@ .interactive-session .chat-used-context { display: flex; flex-direction: column; - gap: 6px; + gap: 2px; } .interactive-response-progress-tree, @@ -626,7 +684,11 @@ border: 1px solid var(--vscode-chat-requestBorder); border-radius: 4px; margin-bottom: 8px; - padding: 4px; + padding: 6px 8px; +} + +.interactive-item-container .chat-notification-widget { + padding: 8px 12px; } .interactive-session .chat-used-context-list .monaco-list .monaco-list-row { @@ -635,8 +697,7 @@ .interactive-session .chat-used-context-label { font-size: 12px; - color: var(--vscode-foreground); - opacity: 0.8; + color: var(--vscode-descriptionForeground); user-select: none; } @@ -647,13 +708,21 @@ .interactive-session .chat-used-context-label .monaco-button { /* unset Button styles */ display: inline-flex; + gap: 4px; width: 100%; border: none; - padding: 0; + border-radius: 4px; + padding: 4px 8px 4px 0; text-align: initial; justify-content: initial; } +.interactive-session .chat-used-context-label .monaco-button:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-foreground); + +} + .interactive-session .chat-used-context-label .monaco-text-button:focus { outline: none; } @@ -676,9 +745,9 @@ color: var(--vscode-descriptionForeground); font-size: 12px; display: flex; - gap: 5px; + gap: 8px; align-items: center; - margin-bottom: 4px; + margin-bottom: 6px; } .interactive-item-container .rendered-markdown.progress-step > p .codicon { @@ -701,7 +770,8 @@ gap: 6px; } -.interactive-item-container .chat-command-button .monaco-button { +.interactive-item-container .chat-command-button .monaco-button, +.chat-confirmation-widget .chat-confirmation-buttons-container .monaco-button { text-align: left; width: initial; padding: 4px 8px; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css index 068ffd872ee..1599c4ffeac 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -10,7 +10,8 @@ .chat-agent-hover-header { display: flex; - gap: 5px; + gap: 8px; + margin-bottom: 4px; } .chat-agent-hover-icon img, @@ -45,14 +46,24 @@ display: flex; } +.chat-agent-hover .chat-agent-hover-warning .codicon { + color: var(--vscode-notificationsWarningIcon-foreground) !important; + margin-right: 3px; +} + +.chat-agent-hover.allowedName .chat-agent-hover-warning { + display: none; +} + .chat-agent-hover-header .chat-agent-hover-name { - font-size: 15px; + font-size: 14px; font-weight: 600; } .chat-agent-hover-extension { display: flex; gap: 6px; + color: var(--vscode-descriptionForeground); } .chat-agent-hover.noExtensionName .chat-agent-hover-separator, @@ -65,14 +76,6 @@ } .chat-agent-hover-description, -.chat-agent-hover-marketplace-button .monaco-text-button { +.chat-agent-hover-warning { font-size: 13px; } - -.chat-agent-hover .chat-agent-hover-marketplace-button .monaco-text-button { - margin-left: 3px; - display: unset; - padding: unset; - border: unset; - line-height: unset; -} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css new file mode 100644 index 00000000000..e244f077dd6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatConfirmationWidget.css @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-confirmation-widget { + border: 1px solid var(--vscode-chat-requestBorder); + border-radius: 4px; + margin-bottom: 16px; + padding: 8px 12px 12px; +} + +.chat-confirmation-widget .chat-confirmation-widget-title { + font-weight: 600; +} + +.chat-confirmation-widget .chat-confirmation-widget-title p { + margin: 0 0 4px 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown p { + margin-top: 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown > :last-child { + margin-bottom: 0px; +} + +.chat-confirmation-widget .chat-confirmation-buttons-container { + display: flex; + gap: 8px; + margin-top: 13px; +} + +.chat-confirmation-widget.hideButtons .chat-confirmation-buttons-container { + display: none; +} diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index c215b0cbe9a..8a57732c95d 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -6,13 +6,13 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, canMergeMarkdownStrings } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentMarkdownContentWithVulnerability, IChatAgentVulnerabilityDetails, IChatContentInlineReference, IChatMarkdownContent } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from 'vs/workbench/contrib/chat/common/chatService'; export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI export function annotateSpecialMarkdownContent(response: ReadonlyArray): ReadonlyArray { - const result: Exclude[] = []; + const result: IChatProgressRenderableResponseContent[] = []; for (const item of response) { const previousItem = result[result.length - 1]; if (item.kind === 'inlineReference') { @@ -20,18 +20,21 @@ export function annotateSpecialMarkdownContent(response: ReadonlyArray${item.content.value}`; if (previousItem?.kind === 'markdownContent') { // Since this is inside a codeblock, it needs to be merged into the previous markdown content. - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; + const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); + result[result.length - 1] = { content: merged, kind: 'markdownContent' }; } else { result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); } diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 609a7c18dbf..43cf0f8e680 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -3,18 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { findLast } from 'vs/base/common/arraysFind'; import { timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { revive } from 'vs/base/common/marshalling'; import { IObservable } from 'vs/base/common/observable'; import { observableValue } from 'vs/base/common/observableInternal/base'; import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { ProviderResult } from 'vs/editor/common/languages'; +import { Command, ProviderResult } from 'vs/editor/common/languages'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -23,15 +25,15 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { asJson, IRequestService } from 'vs/platform/request/common/request'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +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 } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from 'vs/workbench/contrib/chat/common/chatService'; //#region agent service, commands etc export interface IChatAgentHistoryEntry { request: IChatAgentRequest; - response: ReadonlyArray; + response: ReadonlyArray; result: IChatAgentResult; } @@ -57,6 +59,7 @@ export namespace ChatAgentLocation { export interface IChatAgentData { id: string; name: string; + fullName?: string; description?: string; extensionId: ExtensionIdentifier; extensionPublisherId: string; @@ -100,7 +103,6 @@ export interface IChatAgentMetadata { helpTextVariablesPrefix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString; isSecondary?: boolean; // Invoked by ctrl/cmd+enter - fullName?: string; icon?: URI; iconDark?: URI; themeIcon?: ThemeIcon; @@ -109,6 +111,7 @@ export interface IChatAgentMetadata { followupPlaceholder?: string; isSticky?: boolean; requester?: IChatRequesterInformation; + supportsSlowVariables?: boolean; } @@ -122,6 +125,8 @@ export interface IChatAgentRequest { enableCommandDetection?: boolean; variables: IChatRequestVariableData; location: ChatAgentLocation; + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; } export interface IChatAgentResult { @@ -141,6 +146,15 @@ interface IChatAgentEntry { impl?: IChatAgentImplementation; } +export interface IChatAgentCompletionItem { + id: string; + name?: string; + fullName?: string; + icon?: ThemeIcon; + value: unknown; + command?: Command; +} + export interface IChatAgentService { _serviceBrand: undefined; /** @@ -150,9 +164,12 @@ export interface IChatAgentService { registerAgent(id: string, data: IChatAgentData): IDisposable; registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; + registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise): IDisposable; + getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise; invokeAgent(agent: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getAgent(id: string): IChatAgentData | undefined; + getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined; getAgents(): IChatAgentData[]; getActivatedAgents(): Array; getAgentsByName(name: string): IChatAgentData[]; @@ -184,7 +201,7 @@ export class ChatAgentService implements IChatAgentService { private readonly _hasDefaultAgent: IContextKey; constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService); } @@ -250,6 +267,19 @@ export class ChatAgentService implements IChatAgentService { }); } + private _agentCompletionProviders = new Map Promise>(); + + registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise) { + this._agentCompletionProviders.set(id, provider); + return { + dispose: () => { this._agentCompletionProviders.delete(id); } + }; + } + + async getAgentCompletionItems(id: string, query: string, token: CancellationToken) { + return await this._agentCompletionProviders.get(id)?.(query, token) ?? []; + } + updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { const agent = this._getAgentEntry(id); if (!agent?.impl) { @@ -260,7 +290,7 @@ export class ChatAgentService implements IChatAgentService { } getDefaultAgent(location: ChatAgentLocation): IChatAgent | undefined { - return this.getActivatedAgents().find(a => !!a.isDefault && a.locations.includes(location)); + return findLast(this.getActivatedAgents(), a => !!a.isDefault && a.locations.includes(location)); } getContributedDefaultAgent(location: ChatAgentLocation): IChatAgentData | undefined { @@ -280,6 +310,10 @@ export class ChatAgentService implements IChatAgentService { return this._getAgentEntry(id)?.data; } + getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { + return this._agents.find(a => getFullyQualifiedId(a.data) === id)?.data; + } + /** * Returns all agent datas that exist- static registered and dynamic ones. */ @@ -300,7 +334,7 @@ export class ChatAgentService implements IChatAgentService { async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._getAgentEntry(id); if (!data?.impl) { - throw new Error(`No activated agent with id ${id}`); + throw new Error(`No activated agent with id "${id}"`); } return await data.impl.invoke(request, progress, history, token); @@ -309,7 +343,7 @@ export class ChatAgentService implements IChatAgentService { async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._getAgentEntry(id); if (!data?.impl) { - throw new Error(`No activated agent with id ${id}`); + throw new Error(`No activated agent with id "${id}"`); } if (!data.impl?.provideFollowups) { @@ -328,6 +362,7 @@ export class MergedChatAgent implements IChatAgent { get id(): string { return this.data.id; } get name(): string { return this.data.name ?? ''; } + get fullName(): string { return this.data.fullName ?? ''; } get description(): string { return this.data.description ?? ''; } get extensionId(): ExtensionIdentifier { return this.data.extensionId; } get extensionPublisherId(): string { return this.data.extensionPublisherId; } @@ -379,7 +414,7 @@ interface IChatParticipantRegistryResponse { export interface IChatAgentNameService { _serviceBrand: undefined; - getAgentNameRestriction(chatAgentData: IChatAgentData): IObservable; + getAgentNameRestriction(chatAgentData: IChatAgentData): boolean; } export class ChatAgentNameService implements IChatAgentNameService { @@ -444,8 +479,20 @@ export class ChatAgentNameService implements IChatAgentNameService { this.storageService.store(ChatAgentNameService.StorageKey, JSON.stringify(registry), StorageScope.APPLICATION, StorageTarget.MACHINE); } - getAgentNameRestriction(chatAgentData: IChatAgentData): IObservable { - const allowList = this.registry.map(registry => registry[chatAgentData.name.toLowerCase()]); + /** + * Returns true if the agent is allowed to use this name + */ + getAgentNameRestriction(chatAgentData: IChatAgentData): boolean { + // TODO would like to use observables here but nothing uses it downstream and I'm not sure how to combine these two + const nameAllowed = this.checkAgentNameRestriction(chatAgentData.name, chatAgentData).get(); + const fullNameAllowed = !chatAgentData.fullName || this.checkAgentNameRestriction(chatAgentData.fullName.replace(/\s/g, ''), chatAgentData).get(); + return nameAllowed && fullNameAllowed; + } + + private checkAgentNameRestriction(name: string, chatAgentData: IChatAgentData): IObservable { + // Registry is a map of name to an array of extension publisher IDs or extension IDs that are allowed to use it. + // Look up the list of extensions that are allowed to use this name + const allowList = this.registry.map(registry => registry[name.toLowerCase()]); return allowList.map(allowList => { if (!allowList) { return true; @@ -459,3 +506,31 @@ export class ChatAgentNameService implements IChatAgentNameService { this.disposed = true; } } + +export function getFullyQualifiedId(chatAgentData: IChatAgentData): string { + return `${chatAgentData.extensionId.value}.${chatAgentData.id}`; +} + +export function reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData { + const agent = 'name' in raw ? + raw : + { + ...(raw as any), + name: (raw as any).id, + }; + + // Fill in required fields that may be missing from old data + if (!('extensionPublisherId' in agent)) { + agent.extensionPublisherId = agent.extensionPublisher ?? ''; + } + + if (!('extensionDisplayName' in agent)) { + agent.extensionDisplayName = ''; + } + + if (!('extensionId' in agent)) { + agent.extensionId = new ExtensionIdentifier(''); + } + + return revive(agent); +} diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 7a7ffacd78e..7338d036535 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -18,16 +18,21 @@ import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { TextEdit } from 'vs/editor/common/languages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, ChatAgentVoteDirection, isIUsedContext, IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatRequestVariableEntry { + id: string; + fullName?: string; + icon?: ThemeIcon; name: string; + modelDescription?: string; range?: IOffsetRange; value: IChatRequestVariableValue; references?: IChatContentReference[]; + isDynamic?: boolean; } export interface IChatRequestVariableData { @@ -63,7 +68,9 @@ export type IChatProgressResponseContent = | IChatProgressMessage | IChatCommandButton | IChatWarningMessage - | IChatTextEditGroup; + | IChatTask + | IChatTextEditGroup + | IChatConfirmation; export type IChatProgressRenderableResponseContent = Exclude; @@ -90,10 +97,10 @@ export interface IChatResponseModel { readonly isCanceled: boolean; /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ readonly isStale: boolean; - readonly vote: InteractiveSessionVoteDirection | undefined; + readonly vote: ChatAgentVoteDirection | undefined; readonly followups?: IChatFollowup[] | undefined; readonly result?: IChatAgentResult; - setVote(vote: InteractiveSessionVoteDirection): void; + setVote(vote: ChatAgentVoteDirection): void; setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean; } @@ -102,9 +109,10 @@ export class ChatRequestModel implements IChatRequestModel { public response: ChatResponseModel | undefined; - private _id: string; - public get id(): string { - return this._id; + public readonly id: string; + + public get session() { + return this._session; } public get username(): string { @@ -128,12 +136,16 @@ export class ChatRequestModel implements IChatRequestModel { } constructor( - public readonly session: ChatModel, + private _session: ChatModel, public readonly message: IParsedChatRequest, private _variableData: IChatRequestVariableData, private _attempt: number = 0 ) { - this._id = 'request_' + ChatRequestModel.nextId++; + this.id = 'request_' + ChatRequestModel.nextId++; + } + + adoptTo(session: ChatModel) { + this._session = session; } } @@ -169,7 +181,7 @@ export class Response implements IResponse { this._updateRepr(true); } - updateContent(progress: IChatProgressResponseContent | IChatTextEdit, quiet?: boolean): void { + updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatTask, quiet?: boolean): void { if (progress.kind === 'markdownContent') { const responsePartLength = this._responseParts.length - 1; const lastResponsePart = this._responseParts[responsePartLength]; @@ -178,13 +190,7 @@ export class Response implements IResponse { // The last part can't be merged with- not markdown, or markdown with different permissions this._responseParts.push(progress); } else { - lastResponsePart.content = { - value: lastResponsePart.content.value + progress.content.value, - isTrusted: lastResponsePart.content.isTrusted, - supportThemeIcons: lastResponsePart.content.supportThemeIcons, - supportHtml: lastResponsePart.content.supportHtml, - baseUri: lastResponsePart.content.baseUri - } satisfies IMarkdownString; + lastResponsePart.content = appendMarkdownString(lastResponsePart.content, progress.content); } this._updateRepr(quiet); } else if (progress.kind === 'textEdit') { @@ -207,6 +213,25 @@ export class Response implements IResponse { } this._updateRepr(quiet); } + } else if (progress.kind === 'progressTask') { + // Add a new resolving part + const responsePosition = this._responseParts.push(progress) - 1; + this._updateRepr(quiet); + + const disp = progress.onDidAddProgress(() => { + this._updateRepr(false); + }); + + progress.task?.().then((content) => { + // Stop listening for progress updates once the task settles + disp.dispose(); + + // Replace the resolving part's content with the resolved response + if (typeof content === 'string') { + this._responseParts[responsePosition] = { ...progress, content: new MarkdownString(content) }; + } + this._updateRepr(false); + }); } else { this._responseParts.push(progress); @@ -226,6 +251,8 @@ export class Response implements IResponse { return ''; } else if (part.kind === 'progressMessage') { return ''; + } else if (part.kind === 'confirmation') { + return `${part.title}\n${part.message}`; } else { return part.content.value; } @@ -245,9 +272,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel private static nextId = 0; - private _id: string; - public get id(): string { - return this._id; + public readonly id: string; + + public get session() { + return this._session; } public get isComplete(): boolean { @@ -258,7 +286,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._isCanceled; } - public get vote(): InteractiveSessionVoteDirection | undefined { + public get vote(): ChatAgentVoteDirection | undefined { return this._vote; } @@ -320,13 +348,13 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel constructor( _response: IMarkdownString | ReadonlyArray, - public readonly session: ChatModel, + private _session: ChatModel, private _agent: IChatAgentData | undefined, private _slashCommand: IChatAgentCommand | undefined, public readonly requestId: string, private _isComplete: boolean = false, private _isCanceled = false, - private _vote?: InteractiveSessionVoteDirection, + private _vote?: ChatAgentVoteDirection, private _result?: IChatAgentResult, followups?: ReadonlyArray ) { @@ -338,7 +366,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._followups = followups ? [...followups] : undefined; this._response = new Response(_response); this._register(this._response.onDidChangeValue(() => this._onDidChange.fire())); - this._id = 'response_' + ChatResponseModel.nextId++; + this.id = 'response_' + ChatResponseModel.nextId++; } /** @@ -392,7 +420,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._onDidChange.fire(); // Fire so that command followups get rendered on the row } - setVote(vote: InteractiveSessionVoteDirection): void { + setVote(vote: ChatAgentVoteDirection): void { this._vote = vote; this._onDidChange.fire(); } @@ -408,6 +436,11 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._onDidChange.fire(); return true; } + + adoptTo(session: ChatModel) { + this._session = session; + this._onDidChange.fire(); + } } export interface IChatModel { @@ -442,7 +475,7 @@ export interface ISerializableChatRequestData { result?: IChatAgentResult; // Optional for backcompat followups: ReadonlyArray | undefined; isCanceled: boolean | undefined; - vote: InteractiveSessionVoteDirection | undefined; + vote: ChatAgentVoteDirection | undefined; /** For backward compat: should be optional */ usedContext?: IChatUsedContext; contentReferences?: ReadonlyArray; @@ -565,7 +598,7 @@ export class ChatModel extends Disposable implements IChatModel { get responderUsername(): string { return (this._defaultAgent ? - this._defaultAgent.metadata.fullName : + this._defaultAgent.fullName : this.initialData?.responderUsername) ?? ''; } @@ -642,7 +675,7 @@ export class ChatModel extends Disposable implements IChatModel { const request = new ChatRequestModel(this, parsedRequest, variableData); if (raw.response || raw.result || (raw as any).responseErrorDetails) { const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format - this.reviveSerializedAgent(raw.agent) : undefined; + reviveSerializedAgent(raw.agent) : undefined; // Port entries from old format const result = 'responseErrorDetails' in raw ? @@ -669,12 +702,14 @@ export class ChatModel extends Disposable implements IChatModel { ? raw : { variables: [] }; - variableData.variables = variableData.variables.map(v => { + variableData.variables = variableData.variables.map((v): IChatRequestVariableEntry => { if ('values' in v && Array.isArray(v.values)) { return { + id: v.id ?? '', name: v.name, value: v.values[0]?.value, range: v.range, + modelDescription: v.modelDescription, references: v.references }; } else { @@ -685,26 +720,6 @@ export class ChatModel extends Disposable implements IChatModel { return variableData; } - private reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData { - const agent = 'name' in raw ? - raw : - { - ...(raw as any), - name: (raw as any).id, - }; - - // Fill in required fields that may be missing from old data - if (!('extensionPublisherId' in agent)) { - agent.extensionPublisherId = agent.extensionPublisher ?? ''; - } - - if (!('extensionDisplayName' in agent)) { - agent.extensionDisplayName = ''; - } - - return revive(agent); - } - private getParsedRequestFromString(message: string): IParsedChatRequest { // TODO These offsets won't be used, but chat replies need to go through the parser as well const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)]; @@ -769,6 +784,26 @@ export class ChatModel extends Disposable implements IChatModel { return request; } + adoptRequest(request: ChatRequestModel): void { + + // this doesn't use `removeRequest` because it must not dispose the request object + const oldOwner = request.session; + const index = oldOwner._requests.findIndex(candidate => candidate.id === request.id); + + if (index === -1) { + return; + } + + oldOwner._requests.splice(index, 1); + + request.adoptTo(this); + request.response?.adoptTo(this); + this._requests.push(request); + + oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id }); + this._onDidChange.fire({ kind: 'addRequest', request }); + } + acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void { if (!request.response) { request.response = new ChatResponseModel([], this, undefined, undefined, request.id); @@ -778,7 +813,17 @@ export class ChatModel extends Disposable implements IChatModel { throw new Error('acceptResponseProgress: Adding progress to a completed response'); } - if (progress.kind === 'markdownContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || progress.kind === 'markdownVuln' || progress.kind === 'progressMessage' || progress.kind === 'command' || progress.kind === 'textEdit' || progress.kind === 'warning') { + if (progress.kind === 'markdownContent' || + progress.kind === 'treeData' || + progress.kind === 'inlineReference' || + progress.kind === 'markdownVuln' || + progress.kind === 'progressMessage' || + progress.kind === 'command' || + progress.kind === 'textEdit' || + progress.kind === 'warning' || + progress.kind === 'progressTask' || + progress.kind === 'confirmation' + ) { request.response.updateContent(progress, quiet); } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); @@ -931,7 +976,7 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { } public get username(): string { - return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)?.metadata.fullName ?? ''; + return this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel)?.fullName ?? ''; } public get avatarIcon(): ThemeIcon | undefined { @@ -998,3 +1043,14 @@ export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownStri md1.supportHtml === md2.supportHtml && md1.supportThemeIcons === md2.supportThemeIcons; } + +export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString { + const appendedValue = typeof md2 === 'string' ? md2 : md2.value; + return { + value: md1.value + appendedValue, + isTrusted: md1.isTrusted, + supportThemeIcons: md1.supportThemeIcons, + supportHtml: md1.supportHtml, + baseUri: md1.baseUri + }; +} diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index d5643a26534..66bc10c2061 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -6,7 +6,7 @@ import { revive } from 'vs/base/common/marshalling'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentCommand, IChatAgentData, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatSlashData } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -54,7 +54,7 @@ export const chatSubcommandLeader = '/'; export class ChatRequestVariablePart implements IParsedChatRequestPart { static readonly Kind = 'var'; readonly kind = ChatRequestVariablePart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string, readonly variableId: string) { } get text(): string { const argPart = this.variableArg ? `:${this.variableArg}` : ''; @@ -123,7 +123,7 @@ export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { export class ChatRequestDynamicVariablePart implements IParsedChatRequestPart { static readonly Kind = 'dynamic'; readonly kind = ChatRequestDynamicVariablePart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly data: IChatRequestVariableValue) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly id: string, readonly modelDescription: string | undefined, readonly data: IChatRequestVariableValue) { } get referenceText(): string { return this.text.replace(chatVariableLeader, ''); @@ -149,17 +149,12 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed new OffsetRange(part.range.start, part.range.endExclusive), part.editorRange, (part as ChatRequestVariablePart).variableName, - (part as ChatRequestVariablePart).variableArg + (part as ChatRequestVariablePart).variableArg, + (part as ChatRequestVariablePart).variableName || '', ); } else if (part.kind === ChatRequestAgentPart.Kind) { let agent = (part as ChatRequestAgentPart).agent; - if (!('name' in agent)) { - // Port old format - agent = { - ...(agent as any), - name: (agent as any).id - }; - } + agent = reviveSerializedAgent(agent); return new ChatRequestAgentPart( new OffsetRange(part.range.start, part.range.endExclusive), @@ -183,7 +178,9 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed new OffsetRange(part.range.start, part.range.endExclusive), part.editorRange, (part as ChatRequestDynamicVariablePart).text, - revive((part as ChatRequestDynamicVariablePart).data) as any + (part as ChatRequestDynamicVariablePart).id, + (part as ChatRequestDynamicVariablePart).modelDescription, + revive((part as ChatRequestDynamicVariablePart).data) ); } else { throw new Error(`Unknown chat request part: ${part.kind}`); diff --git a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts index 4c4449bbaf9..e6d695c3824 100644 --- a/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParticipantContribTypes.ts @@ -17,6 +17,7 @@ export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook'; export interface IRawChatParticipantContribution { id: string; name: string; + fullName: string; description?: string; isDefault?: boolean; isSticky?: boolean; diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index f60b9cb5dc3..73276b9464d 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -11,7 +11,7 @@ import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynami import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; -const agentReg = /^@([\w_\-]+)(?=(\s|$|\b))/i; // An @-agent +const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command @@ -100,7 +100,13 @@ export class ChatRequestParser { const agentRange = new OffsetRange(offset, offset + full.length); const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const agents = this.agentService.getAgentsByName(name); + let agents = this.agentService.getAgentsByName(name); + if (!agents.length) { + const fqAgent = this.agentService.getAgentByFullyQualifiedId(name); + if (fqAgent) { + agents = [fqAgent]; + } + } // If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the // context and we use that one. Otherwise just pick the first. @@ -141,9 +147,13 @@ export class ChatRequestParser { const variableArg = nextVariableMatch[2] ?? ''; const varRange = new OffsetRange(offset, offset + full.length); const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); + const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + const allowSlow = !usedAgent || usedAgent.agent.metadata.supportsSlowVariables; - if (this.variableService.hasVariable(name)) { - return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg); + // TODO - not really handling duplicate variables names yet + const variable = this.variableService.getVariable(name); + if (variable && (!variable.isSlow || allowSlow)) { + return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg, variable.id); } return; @@ -203,7 +213,7 @@ export class ChatRequestParser { const length = refAtThisPosition.range.endColumn - refAtThisPosition.range.startColumn; const text = message.substring(0, length); const range = new OffsetRange(offset, offset + length); - return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.data); + return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data); } return; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 26a25b167cb..a6921314d18 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; @@ -13,7 +14,7 @@ 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'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IExportableChatData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatParserContext } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -75,7 +76,7 @@ export interface IChatContentVariableReference { export interface IChatContentReference { reference: URI | Location | IChatContentVariableReference; - iconPath?: ThemeIcon | { light: URI; dark: URI }; + iconPath?: ThemeIcon | { light: URI; dark?: URI }; kind: 'reference'; } @@ -106,6 +107,27 @@ export interface IChatProgressMessage { kind: 'progressMessage'; } +export interface IChatTask extends IChatTaskDto { + deferred: DeferredPromise; + progress: (IChatWarningMessage | IChatContentReference)[]; + onDidAddProgress: Event; + add(progress: IChatWarningMessage | IChatContentReference): void; + + complete: (result: string | void) => void; + task: () => Promise; + isSettled: () => boolean; +} + +export interface IChatTaskDto { + content: IMarkdownString; + kind: 'progressTask'; +} + +export interface IChatTaskResult { + content: IMarkdownString | void; + kind: 'progressTaskResult'; +} + export interface IChatWarningMessage { content: IMarkdownString; kind: 'warning'; @@ -133,6 +155,14 @@ export interface IChatTextEdit { kind: 'textEdit'; } +export interface IChatConfirmation { + title: string; + message: string; + data: any; + isUsed?: boolean; + kind: 'confirmation'; +} + export type IChatProgress = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability @@ -142,9 +172,12 @@ export type IChatProgress = | IChatContentInlineReference | IChatAgentDetection | IChatProgressMessage + | IChatTask + | IChatTaskResult | IChatCommandButton | IChatWarningMessage - | IChatTextEdit; + | IChatTextEdit + | IChatConfirmation; export interface IChatFollowup { kind: 'reply'; @@ -155,15 +188,14 @@ export interface IChatFollowup { tooltip?: string; } -// Name has to match the one in vscode.d.ts for some reason -export enum InteractiveSessionVoteDirection { +export enum ChatAgentVoteDirection { Down = 0, Up = 1 } export interface IChatVoteAction { kind: 'vote'; - direction: InteractiveSessionVoteDirection; + direction: ChatAgentVoteDirection; reportIssue?: boolean; } @@ -256,18 +288,28 @@ export interface IChatTransferredSessionData { inputValue: string; } -export interface IChatSendRequestData { +export interface IChatSendRequestResponseState { + responseCreatedPromise: Promise; responseCompletePromise: Promise; +} + +export interface IChatSendRequestData extends IChatSendRequestResponseState { agent: IChatAgentData; slashCommand?: IChatAgentCommand; } export interface IChatSendRequestOptions { - implicitVariablesEnabled?: boolean; location?: ChatAgentLocation; parserContext?: IChatParserContext; attempt?: number; noCommandDetection?: boolean; + acceptedConfirmationData?: any[]; + rejectedConfirmationData?: any[]; + attachedContext?: IChatRequestVariableEntry[]; + + /** The target agent ID can be specified with this property instead of using @ in 'message' */ + agentId?: string; + slashCommand?: string; } export const IChatService = createDecorator('IChatService'); @@ -289,7 +331,7 @@ export interface IChatService { sendRequest(sessionId: string, message: string, options?: IChatSendRequestOptions): Promise; resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; - + adoptRequest(sessionId: string, request: IChatRequestModel): Promise; removeRequest(sessionid: string, requestId: string): Promise; cancelCurrentRequestForSession(sessionId: string): void; clearSession(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 90f32ecbd10..06dbe34e911 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from 'vs/base/common/actions'; 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'; import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; @@ -15,19 +16,18 @@ 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, ICommandService } from 'vs/platform/commands/common/commands'; +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 { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ChatAgentLocation, IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { ChatModel, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, 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, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; 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'; @@ -52,6 +52,7 @@ type ChatProviderInvokedEvent = { chatSessionId: string; agent: string; slashCommand: string | undefined; + location: ChatAgentLocation; }; type ChatProviderInvokedClassification = { @@ -62,46 +63,55 @@ type ChatProviderInvokedClassification = { chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A random ID for the session.' }; agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of agent used.' }; slashCommand?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of slashCommand used.' }; + location?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location at which chat request was made.' }; owner: 'roblourens'; - comment: 'Provides insight into the performance of Chat providers.'; + 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 providers.'; + 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.'; }; @@ -149,8 +159,6 @@ export class ChatService extends Disposable implements IChatService { @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, @IChatAgentService private readonly chatAgentService: IChatAgentService, - @INotificationService private readonly notificationService: INotificationService, - @ICommandService private readonly commandService: ICommandService, ) { super(); @@ -206,22 +214,26 @@ export class ChatService extends Disposable implements IChatService { notifyUserAction(action: IChatUserActionEvent): void { if (action.action.kind === 'vote') { this.telemetryService.publicLog2('interactiveSessionVote', { - direction: action.action.direction === InteractiveSessionVoteDirection.Up ? 'up' : 'down' + 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' + 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 + 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 + commandId, + agentId: action.agentId ?? '' }); } else if (action.action.kind === 'runInTerminal') { this.telemetryService.publicLog2('interactiveSessionRunInTerminal', { @@ -353,18 +365,6 @@ export class ChatService extends Disposable implements IChatService { const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id); if (!defaultAgent) { - // Should have been registered during activation above! - this.notificationService.notify({ - severity: Severity.Error, - message: localize('chatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."), - actions: { - primary: [ - new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => { - return this.commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']); - }) - ] - } - }); throw new ErrorNoTelemetry('No default agent registered'); } const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(model.initialLocation, token) ?? undefined; @@ -426,13 +426,11 @@ export class ChatService extends Disposable implements IChatService { const location = options?.location ?? model.initialLocation; const attempt = options?.attempt ?? 0; const enableCommandDetection = !options?.noCommandDetection; - const implicitVariablesEnabled = options?.implicitVariablesEnabled ?? false; - const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; this.removeRequest(model.sessionId, request.id); - await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, implicitVariablesEnabled, defaultAgent, location); + await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, options).responseCompletePromise; } async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { @@ -457,21 +455,36 @@ export class ChatService extends Disposable implements IChatService { const location = options?.location ?? model.initialLocation; const attempt = options?.attempt ?? 0; - const implicitVariablesEnabled = options?.implicitVariablesEnabled ?? false; const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; - const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, options?.parserContext); + const parsedRequest = this.parseChatRequest(sessionId, request, location, options); const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); // This method is only returning whether the request was accepted - don't block on the actual request return { - responseCompletePromise: this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, implicitVariablesEnabled, defaultAgent, location), + ...this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, defaultAgent, location, options), agent, slashCommand: agentSlashCommandPart?.command, }; } + private parseChatRequest(sessionId: string, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest { + let parserContext = options?.parserContext; + if (options?.agentId) { + const agent = this.chatAgentService.getAgent(options.agentId); + if (!agent) { + throw new Error(`Unknown agent: ${options.agentId}`); + } + parserContext = { selectedAgent: agent }; + const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : ''; + request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`; + } + + const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext); + return parsedRequest; + } + private refreshFollowupsCancellationToken(sessionId: string): CancellationToken { this._sessionFollowupCancelTokens.get(sessionId)?.cancel(); const newTokenSource = new CancellationTokenSource(); @@ -480,7 +493,7 @@ export class ChatService extends Disposable implements IChatService { return newTokenSource.token; } - private async _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, implicitVariablesEnabled: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation): Promise { + private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgent, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState { const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId); let request: ChatRequestModel; const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); @@ -490,6 +503,15 @@ export class ChatService extends Disposable implements IChatService { let gotProgress = false; const requestType = commandPart ? 'slashCommand' : 'string'; + const responseCreated = new DeferredPromise(); + let responseCreatedComplete = false; + function completeResponseCreated(): void { + if (!responseCreatedComplete && request?.response) { + responseCreated.complete(request.response); + responseCreatedComplete = true; + } + } + const source = new CancellationTokenSource(); const token = source.token; const sendRequestInternal = async () => { @@ -507,6 +529,7 @@ export class ChatService extends Disposable implements IChatService { } model.acceptResponseProgress(request, progress); + completeResponseCreated(); }; const stopWatch = new StopWatch(false); @@ -520,7 +543,8 @@ export class ChatService extends Disposable implements IChatService { requestType, agent: agentPart?.agent.id ?? '', slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, - chatSessionId: model.sessionId + chatSessionId: model.sessionId, + location, }); model.cancelRequest(request); @@ -537,17 +561,22 @@ export class ChatService extends Disposable implements IChatService { const initVariableData: IChatRequestVariableData = { variables: [] }; request = model.addRequest(parsedRequest, initVariableData, attempt, agent, agentSlashCommandPart?.command); - const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, progressCallback, token); + completeResponseCreated(); + const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, options?.attachedContext, model, progressCallback, token); request.variableData = variableData; 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 ? { name: v, value } satisfies IChatRequestVariableEntry : + return value ? { id, name: v, value } satisfies IChatRequestVariableEntry : undefined; })); updatedVariableData.variables.push(...coalesce(resolvedImplicitVariables)); @@ -563,7 +592,9 @@ export class ChatService extends Disposable implements IChatService { variables: updatedVariableData, enableCommandDetection, attempt, - location + location, + acceptedConfirmationData: options?.acceptedConfirmationData, + rejectedConfirmationData: options?.rejectedConfirmationData, }; const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); @@ -571,6 +602,7 @@ export class ChatService extends Disposable implements IChatService { agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { request = model.addRequest(parsedRequest, { variables: [] }, attempt); + completeResponseCreated(); // contributed slash commands // TODO: spell this out in the UI const history: IChatMessage[] = []; @@ -611,9 +643,11 @@ export class ChatService extends Disposable implements IChatService { requestType, agent: agentPart?.agent.id ?? '', slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, - chatSessionId: model.sessionId + chatSessionId: model.sessionId, + location }); model.setResponse(request, rawResult); + completeResponseCreated(); this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`); model.completeResponse(request); @@ -623,6 +657,23 @@ export class ChatService extends Disposable implements IChatService { }); } } + } catch (err) { + const result = 'error'; + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + timeToFirstProgress: undefined, + totalTime: undefined, + result, + requestType, + agent: agentPart?.agent.id ?? '', + slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, + chatSessionId: model.sessionId, + location + }); + const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; + model.setResponse(request, rawResult); + completeResponseCreated(); + this.trace('sendRequest', `Error while handling request: ${toErrorMessage(err)}`); + model.completeResponse(request); } finally { listener.dispose(); } @@ -632,7 +683,10 @@ export class ChatService extends Disposable implements IChatService { rawResponsePromise.finally(() => { this._pendingRequests.deleteAndDispose(model.sessionId); }); - return rawResponsePromise; + return { + responseCreatedPromise: responseCreated.p, + responseCompletePromise: rawResponsePromise, + }; } async removeRequest(sessionId: string, requestId: string): Promise { @@ -646,6 +700,28 @@ export class ChatService extends Disposable implements IChatService { model.removeRequest(requestId); } + async adoptRequest(sessionId: string, request: IChatRequestModel) { + if (!(request instanceof ChatRequestModel)) { + throw new TypeError('Can only adopt requests of type ChatRequestModel'); + } + const target = this._sessionModels.get(sessionId); + if (!target) { + throw new Error(`Unknown session: ${sessionId}`); + } + + await target.waitForInitialization(); + + const oldOwner = request.session; + target.adoptRequest(request); + + if (request.response && !request.response.isComplete) { + const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionId); + if (cts) { + this._pendingRequests.set(target.sessionId, cts); + } + } + } + async addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise { this.trace('addCompleteRequest', `message: ${message}`); @@ -688,7 +764,9 @@ export class ChatService extends Disposable implements IChatService { } if (model.initialLocation === ChatAgentLocation.Panel) { - this._persistedSessions[sessionId] = model.toJSON(); + // Turn all the real objects into actual JSON, otherwise, calling 'revive' may fail when it tries to + // assign values to properties that are getters- microsoft/vscode-copilot-release#1233 + this._persistedSessions[sessionId] = JSON.parse(JSON.stringify(model)); } this._sessionModels.deleteAndDispose(sessionId); diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 056620616e0..1df71e988eb 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -5,17 +5,24 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; import { Location } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatContentReference, IChatProgressMessage } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatVariableData { + id: string; name: string; + icon?: ThemeIcon; + fullName?: string; description: string; + modelDescription?: string; + isSlow?: boolean; hidden?: boolean; canTakeArgument?: boolean; } @@ -39,15 +46,21 @@ export interface IChatVariablesService { getVariable(name: string): IChatVariableData | undefined; getVariables(): Iterable>; getDynamicVariables(sessionId: string): ReadonlyArray; // should be its own service? + attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation): void; /** * Resolves all variables that occur in `prompt` */ - resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; + resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; } export interface IDynamicVariable { range: IRange; + id: string; + fullName?: string; + icon?: ThemeIcon; + prefix?: string; + modelDescription?: string; data: IChatRequestVariableValue; } diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 8a39c4d2e63..130ef76979b 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; import { ThemeIcon } from 'vs/base/common/themables'; @@ -11,13 +12,12 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/annotations'; -import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +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 { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatCommandButton, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatCommandButton, IChatConfirmation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTask, IChatUsedContext, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; export function isRequestVM(item: unknown): item is IChatRequestViewModel { return !!item && typeof item === 'object' && 'message' in item; @@ -95,7 +95,13 @@ export interface IChatProgressMessageRenderData { isLast: boolean; } -export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup; +export interface IChatTaskRenderData { + task: IChatTask; + isSettled: boolean; + progressLength: number; +} + +export type IChatRenderData = IChatResponseProgressFileTreeData | IChatResponseMarkdownRenderData | IChatProgressMessageRenderData | IChatCommandButton | IChatTextEditGroup | IChatConfirmation | IChatTaskRenderData | IChatWarningMessage; export interface IChatResponseRenderData { renderedParts: IChatRenderData[]; } @@ -108,6 +114,7 @@ export interface IChatLiveUpdateData { } export interface IChatResponseViewModel { + readonly model: IChatResponseModel; readonly id: string; readonly sessionId: string; /** This ID updates every time the underlying data changes */ @@ -126,15 +133,14 @@ export interface IChatResponseViewModel { readonly isComplete: boolean; readonly isCanceled: boolean; readonly isStale: boolean; - readonly vote: InteractiveSessionVoteDirection | undefined; + readonly vote: ChatAgentVoteDirection | undefined; readonly replyFollowups?: IChatFollowup[]; readonly errorDetails?: IChatResponseErrorDetails; readonly result?: IChatAgentResult; readonly contentUpdateTimings?: IChatLiveUpdateData; renderData?: IChatResponseRenderData; - agentAvatarHasBeenRendered?: boolean; currentRenderedHeight: number | undefined; - setVote(vote: InteractiveSessionVoteDirection): void; + setVote(vote: ChatAgentVoteDirection): void; usedReferencesExpanded?: boolean; vulnerabilitiesListExpanded: boolean; setEditApplied(edit: IChatTextEditGroup, editCount: number): void; @@ -268,24 +274,13 @@ export class ChatViewModel extends Disposable implements IChatViewModel { const renderer = new marked.Renderer(); renderer.code = (value, languageId) => { languageId ??= ''; - const newText = this.fixCodeText(value, languageId); - this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text: newText, languageId }); + this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text: value, languageId }); return ''; }; marked.parse(this.ensureFencedCodeBlocksTerminated(content), { renderer }); } - private fixCodeText(text: string, languageId: string): string { - if (languageId === 'php') { - if (!text.trim().startsWith('<')) { - return ``; - } - } - - return text; - } - /** * Marked doesn't consistently render fenced code blocks that aren't terminated. * @@ -357,6 +352,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; + get model() { + return this._model; + } + get id() { return this._model.id; } @@ -370,6 +369,15 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi } get username() { + if (this.agent) { + const isAllowed = this.chatAgentNameService.getAgentNameRestriction(this.agent); + if (isAllowed) { + return this.agent.fullName || this.agent.name; + } else { + return getFullyQualifiedId(this.agent); + } + } + return this._model.username; } @@ -438,7 +446,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi } renderData: IChatResponseRenderData | undefined = undefined; - agentAvatarHasBeenRendered?: boolean; currentRenderedHeight: number | undefined; private _usedReferencesExpanded: boolean | undefined; @@ -471,6 +478,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi constructor( private readonly _model: IChatResponseModel, @ILogService private readonly logService: ILogService, + @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, ) { super(); @@ -512,7 +520,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi this.logService.trace(`ChatResponseViewModel#${tag}: ${message}`); } - setVote(vote: InteractiveSessionVoteDirection): void { + setVote(vote: ChatAgentVoteDirection): void { this._modelChangeCount++; this._model.setVote(vote); } diff --git a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts index f9431569db7..01716421e7b 100644 --- a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts +++ b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts @@ -7,6 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Memento } from 'vs/workbench/common/memento'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CHAT_PROVIDER_ID } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes'; export interface IChatHistoryEntry { @@ -21,8 +22,8 @@ export interface IChatWidgetHistoryService { readonly onDidClearHistory: Event; clearHistory(): void; - getHistory(): IChatHistoryEntry[]; - saveHistory(history: IChatHistoryEntry[]): void; + getHistory(location: ChatAgentLocation): IChatHistoryEntry[]; + saveHistory(location: ChatAgentLocation, history: IChatHistoryEntry[]): void; } interface IChatHistory { @@ -51,15 +52,23 @@ export class ChatWidgetHistoryService implements IChatWidgetHistoryService { this.viewState = loadedState; } - getHistory(): IChatHistoryEntry[] { - return this.viewState.history?.[CHAT_PROVIDER_ID] ?? []; + getHistory(location: ChatAgentLocation): IChatHistoryEntry[] { + const key = this.getKey(location); + return this.viewState.history?.[key] ?? []; } - saveHistory(history: IChatHistoryEntry[]): void { + private getKey(location: ChatAgentLocation): string { + // Preserve history for panel by continuing to use the same old provider id. Use the location as a key for other chat locations. + return location === ChatAgentLocation.Panel ? CHAT_PROVIDER_ID : location; + } + + saveHistory(location: ChatAgentLocation, history: IChatHistoryEntry[]): void { if (!this.viewState.history) { this.viewState.history = {}; } - this.viewState.history[CHAT_PROVIDER_ID] = history; + + const key = this.getKey(location); + this.viewState.history[key] = history; this.memento.saveMemento(); } diff --git a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts index eeb2d6691fb..94870296160 100644 --- a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts +++ b/src/vs/workbench/contrib/chat/common/chatWordCounter.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const wordSeparatorCharPattern = /[\s\|\-]/; - export interface IWordCountResult { value: string; actualWordCount: number; @@ -12,27 +10,20 @@ export interface IWordCountResult { } export function getNWords(str: string, numWordsToCount: number): IWordCountResult { - let wordCount = numWordsToCount; - let i = 0; - while (i < str.length && wordCount > 0) { - // Consume word separator chars - while (i < str.length && str[i].match(wordSeparatorCharPattern)) { - i++; - } + // Match words and markdown style links + const allWordMatches = Array.from(str.matchAll(/\[([^\]]+)\]\(([^)]+)\)|[^\s\|\-]+/g)); - // Consume word chars - while (i < str.length && !str[i].match(wordSeparatorCharPattern)) { - i++; - } + const targetWords = allWordMatches.slice(0, numWordsToCount); - wordCount--; - } + const endIndex = numWordsToCount > allWordMatches.length + ? str.length // Reached end of string + : targetWords.length ? targetWords.at(-1)!.index + targetWords.at(-1)![0].length : 0; - const value = str.substring(0, i); + const value = str.substring(0, endIndex); return { value, - actualWordCount: numWordsToCount - wordCount, - isFullString: i >= str.length + actualWordCount: targetWords.length === 0 ? (value.length ? 1 : 0) : targetWords.length, + isFullString: endIndex >= str.length }; } diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index f364b9e78ab..a857b64cb1d 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -64,7 +64,7 @@ export class CodeBlockModelCollection extends Disposable { const entry = this.getOrCreate(sessionId, chat, codeBlockIndex); const extractedVulns = extractVulnerabilitiesFromText(content.text); - const newText = extractedVulns.newText; + const newText = fixCodeText(extractedVulns.newText, content.languageId); this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities); const textModel = (await entry.model).textEditorModel; @@ -137,3 +137,13 @@ export class CodeBlockModelCollection extends Disposable { }; } } + +function fixCodeText(text: string, languageId: string | undefined): string { + if (languageId === 'php') { + if (!text.trim().startsWith('<')) { + return `; } +export interface ILanguageModelChatSelector { + readonly name?: string; + readonly identifier?: string; + readonly vendor?: string; + readonly version?: string; + readonly family?: string; + readonly tokens?: number; + readonly extension?: ExtensionIdentifier; +} + export const ILanguageModelsService = createDecorator('ILanguageModelsService'); +export interface ILanguageModelsChangeEvent { + added?: { + identifier: string; + metadata: ILanguageModelChatMetadata; + }[]; + removed?: string[]; +} + export interface ILanguageModelsService { readonly _serviceBrand: undefined; - onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>; + onDidChangeLanguageModels: Event; getLanguageModelIds(): string[]; lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined; + selectLanguageModels(selector: ILanguageModelChatSelector): Promise; + registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable; makeLanguageModelChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, progress: IProgress, token: CancellationToken): Promise; @@ -64,13 +96,94 @@ export interface ILanguageModelsService { computeTokenLength(identifier: string, message: string | IChatMessage, token: CancellationToken): Promise; } +const languageModelType: IJSONSchema = { + type: 'object', + properties: { + vendor: { + type: 'string', + description: localize('vscode.extension.contributes.languageModels.vendor', "A globally unique vendor of language models.") + } + } +}; + +interface IUserFriendlyLanguageModel { + vendor: string; +} + +export const languageModelExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'languageModels', + jsonSchema: { + description: localize('vscode.extension.contributes.languageModels', "Contribute language models of a specific vendor."), + oneOf: [ + languageModelType, + { + type: 'array', + items: languageModelType + } + ] + }, + activationEventsGenerator: (contribs: IUserFriendlyLanguageModel[], result: { push(item: string): void }) => { + for (const contrib of contribs) { + result.push(`onLanguageModelChat:${contrib.vendor}`); + } + } +}); + export class LanguageModelsService implements ILanguageModelsService { + readonly _serviceBrand: undefined; - private readonly _providers: Map = new Map(); + private readonly _providers = new Map(); + private readonly _vendors = new Set(); - private readonly _onDidChangeProviders = new Emitter<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }>(); - readonly onDidChangeLanguageModels: Event<{ added?: ILanguageModelChatMetadata[]; removed?: string[] }> = this._onDidChangeProviders.event; + private readonly _onDidChangeProviders = new Emitter(); + readonly onDidChangeLanguageModels: Event = this._onDidChangeProviders.event; + + constructor( + @IExtensionService private readonly _extensionService: IExtensionService, + @ILogService private readonly _logService: ILogService, + ) { + + languageModelExtensionPoint.setHandler((extensions) => { + + this._vendors.clear(); + + for (const extension of extensions) { + + if (!isProposedApiEnabled(extension.description, 'chatProvider')) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.chatProviderRequired', "This contribution point requires the 'chatProvider' proposal.")); + continue; + } + + for (const item of Iterable.wrap(extension.value)) { + if (this._vendors.has(item.vendor)) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor)); + continue; + } + if (isFalsyOrWhitespace(item.vendor)) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.emptyVendor', "The vendor field cannot be empty.")); + continue; + } + if (item.vendor.trim() !== item.vendor) { + extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace.")); + continue; + } + this._vendors.add(item.vendor); + } + } + + const removed: string[] = []; + for (const [identifier, value] of this._providers) { + if (!this._vendors.has(value.metadata.vendor)) { + this._providers.delete(identifier); + removed.push(identifier); + } + } + if (removed.length > 0) { + this._onDidChangeProviders.fire({ removed }); + } + }); + } dispose() { this._onDidChangeProviders.dispose(); @@ -85,15 +198,62 @@ export class LanguageModelsService implements ILanguageModelsService { return this._providers.get(identifier)?.metadata; } + async selectLanguageModels(selector: ILanguageModelChatSelector): Promise { + + if (selector.vendor) { + // selective activation + await this._extensionService.activateByEvent(`onLanguageModelChat:${selector.vendor}}`); + } else { + // activate all extensions that do language models + const all = Array.from(this._vendors).map(vendor => this._extensionService.activateByEvent(`onLanguageModelChat:${vendor}`)); + await Promise.all(all); + } + + const result: string[] = []; + + for (const [identifier, model] of this._providers) { + + if (selector.vendor !== undefined && model.metadata.vendor === selector.vendor + || selector.family !== undefined && model.metadata.family === selector.family + || selector.version !== undefined && model.metadata.version === selector.version + || selector.identifier !== undefined && model.metadata.id === selector.identifier + || selector.extension !== undefined && model.metadata.targetExtensions?.some(candidate => ExtensionIdentifier.equals(candidate, selector.extension)) + ) { + // true selection + result.push(identifier); + + } else if (!selector || ( + selector.vendor === undefined + && selector.family === undefined + && selector.version === undefined + && selector.identifier === undefined) + ) { + // no selection + result.push(identifier); + } + } + + this._logService.trace('[LM] selected language models', selector, result); + + return result; + } + registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable { + + this._logService.trace('[LM] registering language model chat', identifier, provider.metadata); + + if (!this._vendors.has(provider.metadata.vendor)) { + throw new Error(`Chat response provider uses UNKNOWN vendor ${provider.metadata.vendor}.`); + } if (this._providers.has(identifier)) { throw new Error(`Chat response provider with identifier ${identifier} is already registered.`); } this._providers.set(identifier, provider); - this._onDidChangeProviders.fire({ added: [provider.metadata] }); + this._onDidChangeProviders.fire({ added: [{ identifier, metadata: provider.metadata }] }); return toDisposable(() => { if (this._providers.delete(identifier)) { this._onDidChangeProviders.fire({ removed: [identifier] }); + this._logService.trace('[LM] UNregistered language model chat', identifier, provider.metadata); } }); } diff --git a/src/vs/workbench/contrib/chat/common/voiceChat.ts b/src/vs/workbench/contrib/chat/common/voiceChatService.ts similarity index 86% rename from src/vs/workbench/contrib/chat/common/voiceChat.ts rename to src/vs/workbench/contrib/chat/common/voiceChatService.ts index 67989f188a0..ac140ad3c6b 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChat.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChatService.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { rtrim } from 'vs/base/common/strings'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; @@ -58,6 +60,8 @@ enum PhraseTextType { AGENT_AND_COMMAND = 3 } +export const VoiceChatInProgress = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "A speech-to-text session is in progress for chat.") }); + export class VoiceChatService extends Disposable implements IVoiceChatService { readonly _serviceBrand: undefined; @@ -77,9 +81,13 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { private static readonly CHAT_AGENT_ALIAS = new Map([['vscode', 'code']]); + private readonly voiceChatInProgress = VoiceChatInProgress.bindTo(this.contextKeyService); + private activeVoiceChatSessions = 0; + constructor( @ISpeechService private readonly speechService: ISpeechService, - @IChatAgentService private readonly chatAgentService: IChatAgentService + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(); } @@ -116,7 +124,19 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { async createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): Promise { const disposables = new DisposableStore(); - disposables.add(token.onCancellationRequested(() => disposables.dispose())); + + const onSessionStoppedOrCanceled = (dispose: boolean) => { + this.activeVoiceChatSessions--; + if (this.activeVoiceChatSessions === 0) { + this.voiceChatInProgress.reset(); + } + + if (dispose) { + disposables.dispose(); + } + }; + + disposables.add(token.onCancellationRequested(() => onSessionStoppedOrCanceled(true))); let detectedAgent = false; let detectedSlashCommand = false; @@ -124,6 +144,10 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { const emitter = disposables.add(new Emitter()); const session = await this.speechService.createSpeechToTextSession(token, 'chat'); + if (token.isCancellationRequested) { + onSessionStoppedOrCanceled(true); + } + const phrases = this.createPhrases(options.model); disposables.add(session.onDidChange(e => { switch (e.status) { @@ -193,6 +217,15 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { break; } } + case SpeechToTextStatus.Started: + this.activeVoiceChatSessions++; + this.voiceChatInProgress.set(true); + emitter.fire(e); + break; + case SpeechToTextStatus.Stopped: + onSessionStoppedOrCanceled(false); + emitter.fire(e); + break; default: emitter.fire(e); break; diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css index ba4f2a36513..beae62f5939 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css @@ -4,16 +4,25 @@ *--------------------------------------------------------------------------------------------*/ /* - * Replace with "microphone" icon. + * Replace "loading" with "microphone" icon. */ .monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: var(--vscode-icon-mic-filled-content); font-family: var(--vscode-icon-mic-filled-font-family); } +/* + * Replace "sync" with "pulse" icon. + */ +.monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before { + content: var(--vscode-icon-pulse-content); + font-family: var(--vscode-icon-pulse-font-family); +} + /* * Clear animation styles when reduced motion is enabled. */ +.monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled), .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { animation: none; } @@ -21,6 +30,7 @@ /* * Replace with "stop" icon when reduced motion is enabled. */ +.monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before, .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: var(--vscode-icon-debug-stop-content); font-family: var(--vscode-icon-debug-stop-font-family); 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 2b81361eeed..86454907209 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler, disposableTimeout } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import 'vs/css!./media/voiceChatActions'; +import { RunOnceScheduler, disposableTimeout, raceCancellation } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { assertIsDefined, isNumber } from 'vs/base/common/types'; -import 'vs/css!./media/voiceChatActions'; +import { isNumber } from 'vs/base/common/types'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize, localize2 } from 'vs/nls'; @@ -27,7 +26,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ProgressLocation } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; -import { spinningLoading } from 'vs/platform/theme/common/iconRegistry'; +import { spinningLoading, syncing } from 'vs/platform/theme/common/iconRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; @@ -36,16 +35,17 @@ import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme'; import { AccessibilityVoiceSettingId, SpeechTimeoutDefault, accessibilityConfigurationNodeBase } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; -import { CHAT_VIEW_ID, IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IChatService, KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; -import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; +import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_ENABLED, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { KEYWORD_ACTIVIATION_SETTING_ID } from 'vs/workbench/contrib/chat/common/chatService'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IVoiceChatService, VoiceChatInProgress as GlobalVoiceChatInProgress } from 'vs/workbench/contrib/chat/common/voiceChatService'; import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; +import { HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechStatus, TextToSpeechInProgress as GlobalTextToSpeechInProgress } from 'vs/workbench/contrib/speech/common/speechService'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalChatContextKeys, TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -53,30 +53,45 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; +import { IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { renderStringAsPlaintext } from 'vs/base/browser/markdownRenderer'; -const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey('voiceChatGettingReady', false, { type: 'boolean', description: localize('voiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat.") }); -const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress for voice chat.") }); +//#region Speech to Text -const CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS = new RawContextKey('quickVoiceChatInProgress', false, { type: 'boolean', description: localize('quickVoiceChatInProgress', "True when voice recording from microphone is in progress for quick chat.") }); -const CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS = new RawContextKey('inlineVoiceChatInProgress', false, { type: 'boolean', description: localize('inlineVoiceChatInProgress', "True when voice recording from microphone is in progress for inline chat.") }); -const CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS = new RawContextKey('terminalVoiceChatInProgress', false, { type: 'boolean', description: localize('terminalVoiceChatInProgress', "True when voice recording from microphone is in progress for terminal chat.") }); -const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey('voiceChatInViewInProgress', false, { type: 'boolean', description: localize('voiceChatInViewInProgress', "True when voice recording from microphone is in progress in the chat view.") }); -const CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS = new RawContextKey('voiceChatInEditorInProgress', false, { type: 'boolean', description: localize('voiceChatInEditorInProgress', "True when voice recording from microphone is in progress in the chat editor.") }); +type VoiceChatSessionContext = 'view' | 'inline' | 'terminal' | 'quick' | 'editor'; +const VoiceChatSessionContexts: VoiceChatSessionContext[] = ['view', 'inline', 'terminal', 'quick', 'editor']; +const TerminalChatExecute = MenuId.for('terminalChatInput'); // unfortunately, terminal decided to go with their own menu (https://github.com/microsoft/vscode/issues/208789) + +// Global Context Keys (set on global context key service) const CanVoiceChat = ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, HasSpeechProvider); -const FocusInChatInput = assertIsDefined(ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT)); +const FocusInChatInput = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CONTEXT_IN_CHAT_INPUT); +const AnyChatRequestInProgress = ContextKeyExpr.or(CONTEXT_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, TerminalChatContextKeys.requestActive); -type VoiceChatSessionContext = 'inline' | 'terminal' | 'quick' | 'view' | 'editor'; +// Scoped Context Keys (set on per-chat-context scoped context key service) +const ScopedVoiceChatGettingReady = new RawContextKey('scopedVoiceChatGettingReady', false, { type: 'boolean', description: localize('scopedVoiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat. This key is only defined scoped, per chat context.") }); +const ScopedVoiceChatInProgress = new RawContextKey('scopedVoiceChatInProgress', undefined, { type: 'string', description: localize('scopedVoiceChatInProgress', "Defined as a location where voice recording from microphone is in progress for voice chat. This key is only defined scoped, per chat context.") }); +const AnyScopedVoiceChatInProgress = ContextKeyExpr.or(...VoiceChatSessionContexts.map(context => ScopedVoiceChatInProgress.isEqualTo(context))); + +enum VoiceChatSessionState { + Stopped = 1, + GettingReady, + Started +} interface IVoiceChatSessionController { readonly onDidAcceptInput: Event; - readonly onDidCancelInput: Event; + readonly onDidHideInput: Event; readonly context: VoiceChatSessionContext; + readonly scopedContextKeyService: IContextKeyService; + + updateState(state: VoiceChatSessionState): void; focusInput(): void; - acceptInput(): void; + acceptInput(): Promise; updateInput(text: string): void; getInput(): string; @@ -86,184 +101,137 @@ interface IVoiceChatSessionController { class VoiceChatSessionControllerFactory { - static create(accessor: ServicesAccessor, context: 'inline'): Promise; - static create(accessor: ServicesAccessor, context: 'quick'): Promise; - static create(accessor: ServicesAccessor, context: 'view'): Promise; - static create(accessor: ServicesAccessor, context: 'focused'): Promise; - static create(accessor: ServicesAccessor, context: 'terminal'): Promise; - static create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise; - static async create(accessor: ServicesAccessor, context: 'inline' | 'terminal' | 'quick' | 'view' | 'focused'): Promise { + static async create(accessor: ServicesAccessor, context: 'view' | 'inline' | 'quick' | 'focused'): Promise { const chatWidgetService = accessor.get(IChatWidgetService); - const viewsService = accessor.get(IViewsService); const quickChatService = accessor.get(IQuickChatService); const layoutService = accessor.get(IWorkbenchLayoutService); const editorService = accessor.get(IEditorService); const terminalService = accessor.get(ITerminalService); - - // Currently Focused Context - if (context === 'focused') { - - // Try with the terminal chat - const activeInstance = terminalService.activeInstance; - if (activeInstance) { - const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - if (terminalChat?.hasFocus()) { - return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); - } - } - - // Try with the chat widget service, which currently - // only supports the chat view and quick chat - // https://github.com/microsoft/vscode/issues/191191 - const chatInput = chatWidgetService.lastFocusedWidget; - if (chatInput?.hasInputFocus()) { - // Unfortunately there does not seem to be a better way - // to figure out if the chat widget is in a part or picker - if ( - layoutService.hasFocus(Parts.SIDEBAR_PART) || - layoutService.hasFocus(Parts.PANEL_PART) || - layoutService.hasFocus(Parts.AUXILIARYBAR_PART) - ) { - return VoiceChatSessionControllerFactory.doCreateForChatView(chatInput, viewsService); - } - - if (layoutService.hasFocus(Parts.EDITOR_PART)) { - return VoiceChatSessionControllerFactory.doCreateForChatEditor(chatInput, viewsService); - } - - return VoiceChatSessionControllerFactory.doCreateForQuickChat(chatInput, quickChatService); - } - - // Try with the inline chat - const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); - if (activeCodeEditor) { - const inlineChat = InlineChatController.get(activeCodeEditor); - if (inlineChat?.hasFocus()) { - return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); - } - } - } - - // View Chat - if (context === 'view' || context === 'focused' /* fallback in case 'focused' was not successful */) { - const chatView = await VoiceChatSessionControllerFactory.revealChatView(accessor); - if (chatView) { - return VoiceChatSessionControllerFactory.doCreateForChatView(chatView, viewsService); - } - } - - // Inline Chat - if (context === 'inline') { - const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); - if (activeCodeEditor) { - const inlineChat = InlineChatController.get(activeCodeEditor); - if (inlineChat) { - return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); - } - } - } - - // Terminal Chat - if (context === 'terminal') { - const activeInstance = terminalService.activeInstance; - if (activeInstance) { - const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - if (terminalChat) { - return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); - } - } - } - - // Quick Chat - if (context === 'quick') { - quickChatService.open(); - - const quickChat = chatWidgetService.lastFocusedWidget; - if (quickChat) { - return VoiceChatSessionControllerFactory.doCreateForQuickChat(quickChat, quickChatService); - } - } - - return undefined; - } - - static async revealChatView(accessor: ServicesAccessor): Promise { - const chatService = accessor.get(IChatService); const viewsService = accessor.get(IViewsService); - if (chatService.isEnabled(ChatAgentLocation.Panel)) { - return showChatView(viewsService); + + switch (context) { + case 'focused': { + const controller = VoiceChatSessionControllerFactory.doCreateForFocusedChat(terminalService, chatWidgetService, layoutService); + return controller ?? VoiceChatSessionControllerFactory.create(accessor, 'view'); // fallback to 'view' + } + case 'view': { + const chatWidget = await showChatView(viewsService); + if (chatWidget) { + return VoiceChatSessionControllerFactory.doCreateForChatWidget('view', chatWidget); + } + break; + } + case 'inline': { + const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); + if (activeCodeEditor) { + const inlineChat = InlineChatController.get(activeCodeEditor); + if (inlineChat) { + if (!inlineChat.joinCurrentRun()) { + inlineChat.run(); + } + return VoiceChatSessionControllerFactory.doCreateForChatWidget('inline', inlineChat.chatWidget); + } + } + break; + } + case 'quick': { + quickChatService.open(); // this will populate focused chat widget in the chat widget service + return VoiceChatSessionControllerFactory.create(accessor, 'focused'); + } } return undefined; } - private static doCreateForChatView(chatView: IChatWidget, viewsService: IViewsService): IVoiceChatSessionController { - return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('view', chatView, viewsService); + private static doCreateForFocusedChat(terminalService: ITerminalService, chatWidgetService: IChatWidgetService, layoutService: IWorkbenchLayoutService): IVoiceChatSessionController | undefined { + + // 1.) probe terminal chat which is not part of chat widget service + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat?.hasFocus()) { + return VoiceChatSessionControllerFactory.doCreateForTerminalChat(terminalChat); + } + } + + // 2.) otherwise go via chat widget service + const chatWidget = chatWidgetService.lastFocusedWidget; + if (chatWidget?.hasInputFocus()) { + + // Figure out the context of the chat widget by asking + // layout service for the part that has focus. Unfortunately + // there is no better way because the widget does not know + // its location. + + let context: VoiceChatSessionContext; + if (layoutService.hasFocus(Parts.EDITOR_PART)) { + context = chatWidget.location === ChatAgentLocation.Panel ? 'editor' : 'inline'; + } else if ( + [Parts.SIDEBAR_PART, Parts.PANEL_PART, Parts.AUXILIARYBAR_PART, Parts.TITLEBAR_PART, Parts.STATUSBAR_PART, Parts.BANNER_PART, Parts.ACTIVITYBAR_PART].some(part => layoutService.hasFocus(part)) + ) { + context = 'view'; + } else { + context = 'quick'; + } + + return VoiceChatSessionControllerFactory.doCreateForChatWidget(context, chatWidget); + } + + return undefined; } - private static doCreateForChatEditor(chatView: IChatWidget, viewsService: IViewsService): IVoiceChatSessionController { - return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('editor', chatView, viewsService); + private static createChatContextKeyController(contextKeyService: IContextKeyService, context: VoiceChatSessionContext): (state: VoiceChatSessionState) => void { + const contextVoiceChatGettingReady = ScopedVoiceChatGettingReady.bindTo(contextKeyService); + const contextVoiceChatInProgress = ScopedVoiceChatInProgress.bindTo(contextKeyService); + + return (state: VoiceChatSessionState) => { + switch (state) { + case VoiceChatSessionState.GettingReady: + contextVoiceChatGettingReady.set(true); + contextVoiceChatInProgress.reset(); + break; + case VoiceChatSessionState.Started: + contextVoiceChatGettingReady.reset(); + contextVoiceChatInProgress.set(context); + break; + case VoiceChatSessionState.Stopped: + contextVoiceChatGettingReady.reset(); + contextVoiceChatInProgress.reset(); + break; + } + }; } - private static doCreateForChatViewOrEditor(context: 'view' | 'editor', chatView: IChatWidget, viewsService: IViewsService): IVoiceChatSessionController { + private static doCreateForChatWidget(context: VoiceChatSessionContext, chatWidget: IChatWidget): IVoiceChatSessionController { return { context, - onDidAcceptInput: chatView.onDidAcceptInput, - // TODO@bpasero cancellation needs to work better for chat editors that are not view bound - onDidCancelInput: Event.filter(viewsService.onDidChangeViewVisibility, e => e.id === CHAT_VIEW_ID), - focusInput: () => chatView.focusInput(), - acceptInput: () => chatView.acceptInput(), - updateInput: text => chatView.setInput(text), - getInput: () => chatView.getInput(), - setInputPlaceholder: text => chatView.setInputPlaceholder(text), - clearInputPlaceholder: () => chatView.resetInputPlaceholder() - }; - } - - private static doCreateForQuickChat(quickChat: IChatWidget, quickChatService: IQuickChatService): IVoiceChatSessionController { - return { - context: 'quick', - onDidAcceptInput: quickChat.onDidAcceptInput, - onDidCancelInput: quickChatService.onDidClose, - focusInput: () => quickChat.focusInput(), - acceptInput: () => quickChat.acceptInput(), - updateInput: text => quickChat.setInput(text), - getInput: () => quickChat.getInput(), - setInputPlaceholder: text => quickChat.setInputPlaceholder(text), - clearInputPlaceholder: () => quickChat.resetInputPlaceholder() - }; - } - - private static doCreateForInlineChat(inlineChat: InlineChatController): IVoiceChatSessionController { - const inlineChatSession = inlineChat.joinCurrentRun() ?? inlineChat.run(); - - return { - context: 'inline', - onDidAcceptInput: inlineChat.onDidAcceptInput, - onDidCancelInput: Event.any( - inlineChat.onDidCancelInput, - Event.fromPromise(inlineChatSession) - ), - focusInput: () => inlineChat.focus(), - acceptInput: () => inlineChat.acceptInput(), - updateInput: text => inlineChat.updateInput(text, false), - getInput: () => inlineChat.getInput(), - setInputPlaceholder: text => inlineChat.setPlaceholder(text), - clearInputPlaceholder: () => inlineChat.resetPlaceholder() + scopedContextKeyService: chatWidget.scopedContextKeyService, + onDidAcceptInput: chatWidget.onDidAcceptInput, + onDidHideInput: chatWidget.onDidHide, + focusInput: () => chatWidget.focusInput(), + acceptInput: () => chatWidget.acceptInput(), + updateInput: text => chatWidget.setInput(text), + getInput: () => chatWidget.getInput(), + setInputPlaceholder: text => chatWidget.setInputPlaceholder(text), + clearInputPlaceholder: () => chatWidget.resetInputPlaceholder(), + updateState: VoiceChatSessionControllerFactory.createChatContextKeyController(chatWidget.scopedContextKeyService, context) }; } private static doCreateForTerminalChat(terminalChat: TerminalChatController): IVoiceChatSessionController { + const context = 'terminal'; return { - context: 'terminal', + context, + scopedContextKeyService: terminalChat.scopedContextKeyService, onDidAcceptInput: terminalChat.onDidAcceptInput, - onDidCancelInput: terminalChat.onDidCancelInput, + onDidHideInput: terminalChat.onDidHide, focusInput: () => terminalChat.focus(), acceptInput: () => terminalChat.acceptInput(), updateInput: text => terminalChat.updateInput(text, false), getInput: () => terminalChat.getInput(), setInputPlaceholder: text => terminalChat.setPlaceholder(text), - clearInputPlaceholder: () => terminalChat.resetPlaceholder() + clearInputPlaceholder: () => terminalChat.resetPlaceholder(), + updateState: VoiceChatSessionControllerFactory.createChatContextKeyController(terminalChat.scopedContextKeyService, context) }; } } @@ -292,26 +260,21 @@ class VoiceChatSessions { return VoiceChatSessions.instance; } - private voiceChatInProgressKey = CONTEXT_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private voiceChatGettingReadyKey = CONTEXT_VOICE_CHAT_GETTING_READY.bindTo(this.contextKeyService); - - private quickVoiceChatInProgressKey = CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private inlineVoiceChatInProgressKey = CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private terminalVoiceChatInProgressKey = CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); - private voiceChatInViewInProgressKey = CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.bindTo(this.contextKeyService); - private voiceChatInEditorInProgressKey = CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.bindTo(this.contextKeyService); - private currentVoiceChatSession: IActiveVoiceChatSession | undefined = undefined; private voiceChatSessionIds = 0; constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, @IVoiceChatService private readonly voiceChatService: IVoiceChatService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { } async start(controller: IVoiceChatSessionController, context?: IChatExecuteActionContext): Promise { + + // Stop running text-to-speech or speech-to-text sessions in chats this.stop(); + ChatSynthesizerSessions.getInstance(this.instantiationService).stop(); let disableTimeout = false; @@ -321,7 +284,7 @@ class VoiceChatSessions { controller, disposables: new DisposableStore(), setTimeoutDisabled: (disabled: boolean) => { disableTimeout = disabled; }, - accept: () => session.controller.acceptInput(), + accept: () => this.accept(sessionId), stop: () => this.stop(sessionId, controller.context) }; @@ -329,11 +292,11 @@ class VoiceChatSessions { session.disposables.add(toDisposable(() => cts.dispose(true))); session.disposables.add(controller.onDidAcceptInput(() => this.stop(sessionId, controller.context))); - session.disposables.add(controller.onDidCancelInput(() => this.stop(sessionId, controller.context))); + session.disposables.add(controller.onDidHideInput(() => this.stop(sessionId, controller.context))); controller.focusInput(); - this.voiceChatGettingReadyKey.set(true); + controller.updateState(VoiceChatSessionState.GettingReady); const voiceChatSession = await this.voiceChatService.createVoiceChatSession(cts.token, { usesAgents: controller.context !== 'inline', model: context?.widget?.viewModel?.model }); @@ -344,7 +307,7 @@ class VoiceChatSessions { voiceChatTimeout = SpeechTimeoutDefault; } - const acceptTranscriptionScheduler = session.disposables.add(new RunOnceScheduler(() => session.controller.acceptInput(), voiceChatTimeout)); + const acceptTranscriptionScheduler = session.disposables.add(new RunOnceScheduler(() => this.accept(sessionId), voiceChatTimeout)); session.disposables.add(voiceChatSession.onDidChange(({ status, text, waitingForInput }) => { if (cts.token.isCancellationRequested) { return; @@ -381,26 +344,7 @@ class VoiceChatSessions { } private onDidSpeechToTextSessionStart(controller: IVoiceChatSessionController, disposables: DisposableStore): void { - this.voiceChatGettingReadyKey.set(false); - this.voiceChatInProgressKey.set(true); - - switch (controller.context) { - case 'inline': - this.inlineVoiceChatInProgressKey.set(true); - break; - case 'terminal': - this.terminalVoiceChatInProgressKey.set(true); - break; - case 'quick': - this.quickVoiceChatInProgressKey.set(true); - break; - case 'view': - this.voiceChatInViewInProgressKey.set(true); - break; - case 'editor': - this.voiceChatInEditorInProgressKey.set(true); - break; - } + controller.updateState(VoiceChatSessionState.Started); let dotCount = 0; @@ -425,20 +369,13 @@ class VoiceChatSessions { this.currentVoiceChatSession.controller.clearInputPlaceholder(); + this.currentVoiceChatSession.controller.updateState(VoiceChatSessionState.Stopped); + this.currentVoiceChatSession.disposables.dispose(); this.currentVoiceChatSession = undefined; - - this.voiceChatGettingReadyKey.set(false); - this.voiceChatInProgressKey.set(false); - - this.quickVoiceChatInProgressKey.set(false); - this.inlineVoiceChatInProgressKey.set(false); - this.terminalVoiceChatInProgressKey.set(false); - this.voiceChatInViewInProgressKey.set(false); - this.voiceChatInEditorInProgressKey.set(false); } - accept(voiceChatSessionId = this.voiceChatSessionIds): void { + async accept(voiceChatSessionId = this.voiceChatSessionIds): Promise { if ( !this.currentVoiceChatSession || this.voiceChatSessionIds !== voiceChatSessionId @@ -446,13 +383,33 @@ class VoiceChatSessions { return; } - this.currentVoiceChatSession.controller.acceptInput(); + const controller = this.currentVoiceChatSession.controller; + const response = await controller.acceptInput(); + if (!response) { + return; + } + + if ( + !this.accessibilityService.isScreenReaderOptimized() && // do not auto synthesize when screen reader is active + this.configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) === true + ) { + let context: IVoiceChatSessionController | 'focused'; + if (controller.context === 'inline') { + // TODO@bpasero this is ugly, but the lightweight inline chat turns into + // a different widget as soon as a response comes in, so we fallback to + // picking up from the focused chat widget + context = 'focused'; + } else { + context = controller; + } + ChatSynthesizerSessions.getInstance(this.instantiationService).start(this.instantiationService.invokeFunction(accessor => ChatSynthesizerSessionController.create(accessor, context, response))); + } } } export const VOICE_KEY_HOLD_THRESHOLD = 500; -async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor, target: 'inline' | 'quick' | 'view' | 'focused', context?: IChatExecuteActionContext): Promise { +async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor, target: 'view' | 'inline' | 'quick' | 'focused', context?: IChatExecuteActionContext): Promise { const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); @@ -480,7 +437,7 @@ async function startVoiceChatWithHoldMode(id: string, accessor: ServicesAccessor class VoiceChatWithHoldModeAction extends Action2 { - constructor(desc: Readonly, private readonly target: 'inline' | 'quick' | 'view') { + constructor(desc: Readonly, private readonly target: 'view' | 'inline' | 'quick') { super(desc); } @@ -496,9 +453,12 @@ export class VoiceChatInChatViewAction extends VoiceChatWithHoldModeAction { constructor() { super({ id: VoiceChatInChatViewAction.ID, - title: localize2('workbench.action.chat.voiceChatInView.label', "Voice Chat in View"), + title: localize2('workbench.action.chat.voiceChatInView.label', "Voice Chat in Chat View"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate() // disable when a chat request is in progress + ), f1: true }, 'view'); } @@ -511,14 +471,15 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { constructor() { super({ id: HoldToVoiceChatInChatViewAction.ID, - title: localize2('workbench.action.chat.holdToVoiceChatInChatView.label', "Hold to Voice Chat in View"), + title: localize2('workbench.action.chat.holdToVoiceChatInChatView.label', "Hold to Voice Chat in Chat View"), keybinding: { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( CanVoiceChat, - FocusInChatInput.negate(), // when already in chat input, disable this action and prefer to start voice chat directly - EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding - NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook keybinding + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), // disable when a chat request is in progress + FocusInChatInput?.negate(), // when already in chat input, disable this action and prefer to start voice chat directly + EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook keybinding ), primary: KeyMod.CtrlCmd | KeyCode.KeyI } @@ -533,6 +494,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); + const viewsService = accessor.get(IViewsService); const holdMode = keybindingService.enableKeybindingHoldMode(HoldToVoiceChatInChatViewAction.ID); @@ -545,7 +507,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { } }, VOICE_KEY_HOLD_THRESHOLD); - (await VoiceChatSessionControllerFactory.revealChatView(accessor))?.focusInput(); + (await showChatView(viewsService))?.focusInput(); await holdMode; handle.dispose(); @@ -565,7 +527,11 @@ export class InlineVoiceChatAction extends VoiceChatWithHoldModeAction { id: InlineVoiceChatAction.ID, title: localize2('workbench.action.chat.inlineVoiceChat', "Inline Voice Chat"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, ActiveEditorContext, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + ActiveEditorContext, + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate() // disable when a chat request is in progress + ), f1: true }, 'inline'); } @@ -580,7 +546,10 @@ export class QuickVoiceChatAction extends VoiceChatWithHoldModeAction { id: QuickVoiceChatAction.ID, title: localize2('workbench.action.chat.quickVoiceChat.label', "Quick Voice Chat"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate() // disable when a chat request is in progress + ), f1: true }, 'quick'); } @@ -600,26 +569,34 @@ export class StartVoiceChatAction extends Action2 { weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and( FocusInChatInput, // scope this action to chat input fields only - EditorContextKeys.focus.negate(), // do not steal the inline-chat keybinding - NOTEBOOK_EDITOR_FOCUSED.negate(), // do not steal the notebook keybinding - CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), - CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), - CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate(), - CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate(), - CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate() + EditorContextKeys.focus.negate(), // do not steal the editor inline-chat keybinding + NOTEBOOK_EDITOR_FOCUSED.negate() // do not steal the notebook inline-chat keybinding ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, icon: Codicon.mic, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_GETTING_READY.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.negate(), TerminalChatContextKeys.requestActive.negate()), + precondition: ContextKeyExpr.and( + CanVoiceChat, + ScopedVoiceChatGettingReady.negate(), // disable when voice chat is getting ready + AnyChatRequestInProgress?.negate(), // disable when any chat request is in progress + SpeechToTextInProgress.negate() // disable when speech to text is in progress + ), menu: [{ id: MenuId.ChatExecute, - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate()), + when: ContextKeyExpr.and( + HasSpeechProvider, + ScopedChatSynthesisInProgress.negate(), // hide when text to speech is in progress + AnyScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress + ), group: 'navigation', order: -1 }, { - id: MenuId.for('terminalChatInput'), - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate()), + id: TerminalChatExecute, + when: ContextKeyExpr.and( + HasSpeechProvider, + ScopedChatSynthesisInProgress.negate(), // hide when text to speech is in progress + AnyScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress + ), group: 'navigation', order: -1 }] @@ -633,9 +610,6 @@ export class StartVoiceChatAction extends Action2 { // from a toolbar within the chat widget, then make sure // to move focus into the input field so that the controller // is properly retrieved - // TODO@bpasero this will actually not work if the button - // is clicked from the inline editor while focus is in a - // chat input field in a view or picker widget.focusInput(); } @@ -643,29 +617,31 @@ export class StartVoiceChatAction extends Action2 { } } -const InstallingSpeechProvider = new RawContextKey('installingSpeechProvider', false, true); +export class StopListeningAction extends Action2 { -export class InstallVoiceChatAction extends Action2 { - - static readonly ID = 'workbench.action.chat.installVoiceChat'; - - private static readonly SPEECH_EXTENSION_ID = 'ms-vscode.vscode-speech'; + static readonly ID = 'workbench.action.chat.stopListening'; constructor() { super({ - id: InstallVoiceChatAction.ID, - title: localize2('workbench.action.chat.startVoiceChat.label', "Start Voice Chat"), + id: StopListeningAction.ID, + title: localize2('workbench.action.chat.stopListening.label', "Stop Listening"), category: CHAT_CATEGORY, - icon: Codicon.mic, - precondition: InstallingSpeechProvider.negate(), + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + primary: KeyCode.Escape, + when: AnyScopedVoiceChatInProgress + }, + icon: spinningLoading, + precondition: GlobalVoiceChatInProgress, // need global context here because of `f1: true` menu: [{ id: MenuId.ChatExecute, - when: HasSpeechProvider.negate(), + when: AnyScopedVoiceChatInProgress, group: 'navigation', order: -1 }, { - id: MenuId.for('terminalChatInput'), - when: HasSpeechProvider.negate(), + id: TerminalChatExecute, + when: AnyScopedVoiceChatInProgress, group: 'navigation', order: -1 }] @@ -673,93 +649,7 @@ export class InstallVoiceChatAction extends Action2 { } async run(accessor: ServicesAccessor): Promise { - const contextKeyService = accessor.get(IContextKeyService); - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - try { - InstallingSpeechProvider.bindTo(contextKeyService).set(true); - await extensionsWorkbenchService.install(InstallVoiceChatAction.SPEECH_EXTENSION_ID, { - justification: localize('confirmInstallDetail', "Microphone support requires this extension."), - enable: true - }, ProgressLocation.Notification); - } finally { - InstallingSpeechProvider.bindTo(contextKeyService).set(false); - } - } -} - -class BaseStopListeningAction extends Action2 { - - constructor( - desc: { id: string; icon?: ThemeIcon; f1?: boolean }, - private readonly target: 'inline' | 'terminal' | 'quick' | 'view' | 'editor' | undefined, - context: RawContextKey, - menu: MenuId | undefined, - ) { - super({ - ...desc, - title: localize2('workbench.action.chat.stopListening.label', "Stop Listening"), - category: CHAT_CATEGORY, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib + 100, - primary: KeyCode.Escape - }, - precondition: ContextKeyExpr.and(CanVoiceChat, context), - menu: menu ? [{ - id: menu, - when: ContextKeyExpr.and(CanVoiceChat, context), - group: 'navigation', - order: -1 - }] : undefined - }); - } - - async run(accessor: ServicesAccessor, context?: IChatExecuteActionContext): Promise { - VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(undefined, this.target); - } -} - -export class StopListeningAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListening'; - - constructor() { - super({ id: StopListeningAction.ID, f1: true }, undefined, CONTEXT_VOICE_CHAT_IN_PROGRESS, undefined); - } -} - -export class StopListeningInChatViewAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInChatView'; - - constructor() { - super({ id: StopListeningInChatViewAction.ID, icon: spinningLoading }, 'view', CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS, MenuId.ChatExecute); - } -} - -export class StopListeningInChatEditorAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInChatEditor'; - - constructor() { - super({ id: StopListeningInChatEditorAction.ID, icon: spinningLoading }, 'editor', CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS, MenuId.ChatExecute); - } -} - -export class StopListeningInQuickChatAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInQuickChat'; - - constructor() { - super({ id: StopListeningInQuickChatAction.ID, icon: spinningLoading }, 'quick', CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS, MenuId.ChatExecute); - } -} - -export class StopListeningInTerminalChatAction extends BaseStopListeningAction { - - static readonly ID = 'workbench.action.chat.stopListeningInTerminalChat'; - - constructor() { - super({ id: StopListeningInTerminalChatAction.ID, icon: spinningLoading }, 'terminal', CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS, MenuId.for('terminalChatInput')); + VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(); } } @@ -778,7 +668,7 @@ export class StopListeningAndSubmitAction extends Action2 { when: FocusInChatInput, primary: KeyMod.CtrlCmd | KeyCode.KeyI }, - precondition: ContextKeyExpr.and(CanVoiceChat, CONTEXT_VOICE_CHAT_IN_PROGRESS) + precondition: GlobalVoiceChatInProgress // need global context here because of `f1: true` }); } @@ -787,65 +677,307 @@ export class StopListeningAndSubmitAction extends Action2 { } } -registerThemingParticipant((theme, collector) => { - let activeRecordingColor: Color | undefined; - let activeRecordingDimmedColor: Color | undefined; - if (theme.type === ColorScheme.LIGHT || theme.type === ColorScheme.DARK) { - activeRecordingColor = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND) ?? theme.getColor(focusBorder); - activeRecordingDimmedColor = activeRecordingColor?.transparent(0.38); - } else { - activeRecordingColor = theme.getColor(contrastBorder); - activeRecordingDimmedColor = theme.getColor(contrastBorder); +//#endregion + +//#region Text to Speech + +const ScopedChatSynthesisInProgress = new RawContextKey('scopedChatSynthesisInProgress', false, { type: 'boolean', description: localize('scopedChatSynthesisInProgress', "Defined as a location where voice recording from microphone is in progress for voice chat. This key is only defined scoped, per chat context.") }); + +interface IChatSynthesizerSessionController { + + readonly onDidHideChat: Event; + + readonly contextKeyService: IContextKeyService; + readonly response: IChatResponseModel; +} + +class ChatSynthesizerSessionController { + + static create(accessor: ServicesAccessor, context: IVoiceChatSessionController | 'focused', response: IChatResponseModel): IChatSynthesizerSessionController { + if (context === 'focused') { + return ChatSynthesizerSessionController.doCreateForFocusedChat(accessor, response); + } else { + return { + onDidHideChat: context.onDidHideInput, + contextKeyService: context.scopedContextKeyService, + response + }; + } } - // Show a "microphone" icon when recording is in progress that glows via outline. - collector.addRule(` - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { - color: ${activeRecordingColor}; - outline: 1px solid ${activeRecordingColor}; - outline-offset: -1px; - animation: pulseAnimation 1s infinite; - border-radius: 50%; - } + private static doCreateForFocusedChat(accessor: ServicesAccessor, response: IChatResponseModel): IChatSynthesizerSessionController { + const chatWidgetService = accessor.get(IChatWidgetService); + const contextKeyService = accessor.get(IContextKeyService); + const terminalService = accessor.get(ITerminalService); - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { - position: absolute; - outline: 1px solid ${activeRecordingColor}; - outline-offset: 2px; - border-radius: 50%; - width: 16px; - height: 16px; - } - - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { - outline: 2px solid ${activeRecordingColor}; - outline-offset: -1px; - animation: pulseAnimation 1500ms cubic-bezier(0.75, 0, 0.25, 1) infinite; - } - - .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { - position: absolute; - outline: 1px solid ${activeRecordingColor}; - outline-offset: 2px; - border-radius: 50%; - width: 16px; - height: 16px; - } - - @keyframes pulseAnimation { - 0% { - outline-width: 2px; - } - 62% { - outline-width: 5px; - outline-color: ${activeRecordingDimmedColor}; - } - 100% { - outline-width: 2px; + // 1.) probe terminal chat which is not part of chat widget service + const activeInstance = terminalService.activeInstance; + if (activeInstance) { + const terminalChat = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + if (terminalChat?.hasFocus()) { + return { + onDidHideChat: terminalChat.onDidHide, + contextKeyService: terminalChat.scopedContextKeyService, + response + }; } } - `); -}); + + // 2.) otherwise go via chat widget service + let chatWidget = chatWidgetService.getWidgetBySessionId(response.session.sessionId); + if (chatWidget?.location === ChatAgentLocation.Editor) { + // TODO@bpasero workaround for https://github.com/microsoft/vscode/issues/212785 + chatWidget = chatWidgetService.lastFocusedWidget; + } + + return { + onDidHideChat: chatWidget?.onDidHide ?? Event.None, + contextKeyService: chatWidget?.scopedContextKeyService ?? contextKeyService, + response + }; + } +} + +class ChatSynthesizerSessions { + + private static instance: ChatSynthesizerSessions | undefined = undefined; + static getInstance(instantiationService: IInstantiationService): ChatSynthesizerSessions { + if (!ChatSynthesizerSessions.instance) { + ChatSynthesizerSessions.instance = instantiationService.createInstance(ChatSynthesizerSessions); + } + + return ChatSynthesizerSessions.instance; + } + + private activeSession: CancellationTokenSource | undefined = undefined; + + constructor( + @ISpeechService private readonly speechService: ISpeechService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async start(controller: IChatSynthesizerSessionController): Promise { + + // Stop running text-to-speech or speech-to-text sessions in chats + this.stop(); + VoiceChatSessions.getInstance(this.instantiationService).stop(); + + const activeSession = this.activeSession = new CancellationTokenSource(); + + const disposables = new DisposableStore(); + activeSession.token.onCancellationRequested(() => disposables.dispose()); + + const session = await this.speechService.createTextToSpeechSession(activeSession.token, 'chat'); + + if (activeSession.token.isCancellationRequested) { + return; + } + + disposables.add(controller.onDidHideChat(() => this.stop())); + + const scopedChatToSpeechInProgress = ScopedChatSynthesisInProgress.bindTo(controller.contextKeyService); + disposables.add(toDisposable(() => scopedChatToSpeechInProgress.reset())); + + disposables.add(session.onDidChange(e => { + switch (e.status) { + case TextToSpeechStatus.Started: + scopedChatToSpeechInProgress.set(true); + break; + case TextToSpeechStatus.Stopped: + scopedChatToSpeechInProgress.reset(); + break; + } + })); + + for await (const chunk of this.nextChatResponseChunk(controller.response, activeSession.token)) { + if (activeSession.token.isCancellationRequested) { + return; + } + + await raceCancellation(session.synthesize(chunk), activeSession.token); + } + } + + private async *nextChatResponseChunk(response: IChatResponseModel, token: CancellationToken): AsyncIterable { + let totalOffset = 0; + let complete = false; + do { + const responseLength = response.response.asString().length; + const { chunk, offset } = this.parseNextChatResponseChunk(response, totalOffset); + totalOffset = offset; + complete = response.isComplete; + + if (chunk) { + yield chunk; + } + + if (token.isCancellationRequested) { + return; + } + + if (!complete && responseLength === response.response.asString().length) { + await raceCancellation(Event.toPromise(response.onDidChange), token); // wait for the response to change + } + } while (!token.isCancellationRequested && !complete); + } + + private parseNextChatResponseChunk(response: IChatResponseModel, offset: number): { readonly chunk: string | undefined; readonly offset: number } { + let chunk: string | undefined = undefined; + + const text = response.response.asString(); + + if (response.isComplete) { + chunk = text.substring(offset); + offset = text.length + 1; + } else { + const res = parseNextChatResponseChunk(text, offset); + chunk = res.chunk; + offset = res.offset; + } + + return { + chunk: chunk ? renderStringAsPlaintext({ value: chunk }) : chunk, // convert markdown to plain text + offset + }; + } + + stop(): void { + this.activeSession?.dispose(true); + this.activeSession = undefined; + } +} + +const sentenceDelimiter = ['.', '!', '?', ':']; +const lineDelimiter = '\n'; +const wordDelimiter = ' '; + +export function parseNextChatResponseChunk(text: string, offset: number): { readonly chunk: string | undefined; readonly offset: number } { + let chunk: string | undefined = undefined; + + for (let i = text.length - 1; i >= offset; i--) { // going from end to start to produce largest chunks + const cur = text[i]; + const next = text[i + 1]; + if ( + sentenceDelimiter.includes(cur) && next === wordDelimiter || // end of sentence + lineDelimiter === cur // end of line + ) { + chunk = text.substring(offset, i + 1).trim(); + offset = i + 1; + break; + } + } + + return { chunk, offset }; +} + +export class ReadChatResponseAloud extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.readChatResponseAloud', + title: localize2('workbench.action.chat.readChatResponseAloud', "Read Aloud"), + icon: Codicon.unmute, + precondition: CanVoiceChat, + menu: { + id: MenuId.ChatMessageTitle, + when: ContextKeyExpr.and( + CanVoiceChat, + CONTEXT_RESPONSE, // only for responses + ScopedChatSynthesisInProgress.negate(), // but not when already in progress + CONTEXT_RESPONSE_FILTERED.negate() // and not when response is filtered + ), + group: 'navigation' + } + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + const instantiationService = accessor.get(IInstantiationService); + + const response = args[0]; + if (!isResponseVM(response)) { + return; + } + + const controller = ChatSynthesizerSessionController.create(accessor, 'focused', response.model); + ChatSynthesizerSessions.getInstance(instantiationService).start(controller); + } +} + +export class StopReadAloud extends Action2 { + + static readonly ID = 'workbench.action.speech.stopReadAloud'; + + constructor() { + super({ + id: StopReadAloud.ID, + icon: syncing, + title: localize2('workbench.action.speech.stopReadAloud', "Stop Reading Aloud"), + f1: true, + category: CHAT_CATEGORY, + precondition: GlobalTextToSpeechInProgress, // need global context here because of `f1: true` + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + primary: KeyCode.Escape, + when: ScopedChatSynthesisInProgress + }, + menu: [ + { + id: MenuId.ChatExecute, + when: ScopedChatSynthesisInProgress, + group: 'navigation', + order: -1 + }, + { + id: TerminalChatExecute, + when: ScopedChatSynthesisInProgress, + group: 'navigation', + order: -1 + } + ] + }); + } + + async run(accessor: ServicesAccessor) { + ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).stop(); + } +} + +export class StopReadChatItemAloud extends Action2 { + + static readonly ID = 'workbench.action.chat.stopReadChatItemAloud'; + + constructor() { + super({ + id: StopReadChatItemAloud.ID, + icon: Codicon.mute, + title: localize2('workbench.action.chat.stopReadChatItemAloud', "Stop Reading Aloud"), + precondition: ScopedChatSynthesisInProgress, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + primary: KeyCode.Escape, + }, + menu: [ + { + id: MenuId.ChatMessageTitle, + when: ContextKeyExpr.and( + ScopedChatSynthesisInProgress, // only when in progress + CONTEXT_RESPONSE, // only for responses + CONTEXT_RESPONSE_FILTERED.negate() // but not when response is filtered + ), + group: 'navigation' + } + ] + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + ChatSynthesizerSessions.getInstance(accessor.get(IInstantiationService)).stop(); + } +} + +//#endregion + +//#region Keyword Recognition function supportsKeywordActivation(configurationService: IConfigurationService, speechService: ISpeechService, chatAgentService: IChatAgentService): boolean { if (!speechService.hasSpeechProvider || !chatAgentService.getDefaultAgent(ChatAgentLocation.Panel)) { @@ -1086,3 +1218,148 @@ class KeywordActivationStatusEntry extends Disposable { this.entry.value?.update(this.getStatusEntryProperties()); } } + +//#endregion + +//#region Install Provider Actions + +const InstallingSpeechProvider = new RawContextKey('installingSpeechProvider', false, true); + +abstract class BaseInstallSpeechProviderAction extends Action2 { + + private static readonly SPEECH_EXTENSION_ID = 'ms-vscode.vscode-speech'; + + async run(accessor: ServicesAccessor): Promise { + const contextKeyService = accessor.get(IContextKeyService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + try { + InstallingSpeechProvider.bindTo(contextKeyService).set(true); + await extensionsWorkbenchService.install(BaseInstallSpeechProviderAction.SPEECH_EXTENSION_ID, { + justification: this.getJustification(), + enable: true + }, ProgressLocation.Notification); + } finally { + InstallingSpeechProvider.bindTo(contextKeyService).reset(); + } + } + + protected abstract getJustification(): string; +} + +export class InstallSpeechProviderForVoiceChatAction extends BaseInstallSpeechProviderAction { + + static readonly ID = 'workbench.action.chat.installProviderForVoiceChat'; + + constructor() { + super({ + id: InstallSpeechProviderForVoiceChatAction.ID, + title: localize2('workbench.action.chat.installProviderForVoiceChat.label', "Start Voice Chat"), + icon: Codicon.mic, + precondition: InstallingSpeechProvider.negate(), + menu: [{ + id: MenuId.ChatExecute, + when: HasSpeechProvider.negate(), + group: 'navigation', + order: -1 + }, { + id: TerminalChatExecute, + when: HasSpeechProvider.negate(), + group: 'navigation', + order: -1 + }] + }); + } + + protected getJustification(): string { + return localize('installProviderForVoiceChat.justification', "Microphone support requires this extension."); + } +} + +export class InstallSpeechProviderForSynthesizeChatAction extends BaseInstallSpeechProviderAction { + + static readonly ID = 'workbench.action.chat.installProviderForSynthesis'; + + constructor() { + super({ + id: InstallSpeechProviderForSynthesizeChatAction.ID, + title: localize2('workbench.action.chat.installProviderForSynthesis.label', "Read Aloud"), + icon: Codicon.unmute, + precondition: InstallingSpeechProvider.negate(), + menu: [{ + id: MenuId.ChatMessageTitle, + when: HasSpeechProvider.negate(), + group: 'navigation' + }] + }); + } + + protected getJustification(): string { + return localize('installProviderForSynthesis.justification', "Speaker support requires this extension."); + } +} + +//#endregion + +registerThemingParticipant((theme, collector) => { + let activeRecordingColor: Color | undefined; + let activeRecordingDimmedColor: Color | undefined; + if (theme.type === ColorScheme.LIGHT || theme.type === ColorScheme.DARK) { + activeRecordingColor = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND) ?? theme.getColor(focusBorder); + activeRecordingDimmedColor = activeRecordingColor?.transparent(0.38); + } else { + activeRecordingColor = theme.getColor(contrastBorder); + activeRecordingDimmedColor = theme.getColor(contrastBorder); + } + + // Show a "microphone" or "pulse" icon when speech-to-text or text-to-speech is in progress that glows via outline. + collector.addRule(` + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled), + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { + color: ${activeRecordingColor}; + outline: 1px solid ${activeRecordingColor}; + outline-offset: -1px; + animation: pulseAnimation 1s infinite; + border-radius: 50%; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; + border-radius: 50%; + width: 16px; + height: 16px; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::after, + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { + outline: 2px solid ${activeRecordingColor}; + outline-offset: -1px; + animation: pulseAnimation 1500ms cubic-bezier(0.75, 0, 0.25, 1) infinite; + } + + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-sync.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + position: absolute; + outline: 1px solid ${activeRecordingColor}; + outline-offset: 2px; + border-radius: 50%; + width: 16px; + height: 16px; + } + + @keyframes pulseAnimation { + 0% { + outline-width: 2px; + } + 62% { + outline-width: 5px; + outline-color: ${activeRecordingDimmedColor}; + } + 100% { + outline-width: 2px; + } + } + `); +}); diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts index 186a6a715c1..9fca2cd9497 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/chat.contribution.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction, HoldToVoiceChatInChatViewAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallSpeechProviderForSynthesizeChatAction, InstallSpeechProviderForVoiceChatAction, HoldToVoiceChatInChatViewAction, ReadChatResponseAloud, StopReadAloud, StopReadChatItemAloud } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; registerAction2(StartVoiceChatAction); -registerAction2(InstallVoiceChatAction); +registerAction2(InstallSpeechProviderForVoiceChatAction); registerAction2(VoiceChatInChatViewAction); registerAction2(HoldToVoiceChatInChatViewAction); @@ -18,9 +18,9 @@ registerAction2(InlineVoiceChatAction); registerAction2(StopListeningAction); registerAction2(StopListeningAndSubmitAction); -registerAction2(StopListeningInChatViewAction); -registerAction2(StopListeningInChatEditorAction); -registerAction2(StopListeningInQuickChatAction); -registerAction2(StopListeningInTerminalChatAction); +registerAction2(ReadChatResponseAloud); +registerAction2(StopReadChatItemAloud); +registerAction2(StopReadAloud); +registerAction2(InstallSpeechProviderForSynthesizeChatAction); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap new file mode 100644 index 00000000000..67f63f14b70 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap @@ -0,0 +1 @@ +
<!--[CDATA[<div-->content]]>
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap new file mode 100644 index 00000000000..c1ba30be800 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap @@ -0,0 +1 @@ +
<!-- comment1 <div></div> --><div>content</div><!-- comment2 -->
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap new file mode 100644 index 00000000000..02c52ac2aa4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap @@ -0,0 +1 @@ +
1<canvas>2<div>3</div></canvas>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 new file mode 100644 index 00000000000..67381fee546 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap @@ -0,0 +1 @@ +
1<div id="id1" style="display: none">2<div id="my id 2">3</div></div>4
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap new file mode 100644 index 00000000000..a58ce687e96 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap @@ -0,0 +1,8 @@ +

heading

+<div> +
    +
  • <div>1</div>
  • +
  • hi
  • +
+</div> +
<canvas>canvas here</canvas>
<details></details>
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap new file mode 100644 index 00000000000..247cce5ff8e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images.0.snap @@ -0,0 +1 @@ +
<div><img src="http://disallowed.com/image.jpg"></div>
\ 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 new file mode 100644 index 00000000000..023b2e6a846 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap @@ -0,0 +1 @@ +
<area>

<input type="text" value="test">
\ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap new file mode 100644 index 00000000000..2e65efe2a14 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap new file mode 100644 index 00000000000..df6a95f4b5d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap @@ -0,0 +1,6 @@ +

heading

+
    +
  • 1
  • +
  • hi
  • +
+
code here
\ 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 new file mode 100644 index 00000000000..f006d1afbf7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { assertSnapshot } from 'vs/base/test/common/snapshot'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ChatMarkdownRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownRenderer'; +import { ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; +import { MockTrustedDomainService } from 'vs/workbench/contrib/url/test/browser/mockTrustedDomainService'; +import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +suite('ChatMarkdownRenderer', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let testRenderer: ChatMarkdownRenderer; + setup(() => { + const instantiationService = store.add(workbenchInstantiationService(undefined, store)); + instantiationService.stub(ITrustedDomainService, new MockTrustedDomainService(['http://allowed.com'])); + testRenderer = instantiationService.createInstance(ChatMarkdownRenderer, {}); + }); + + test('simple', async () => { + const md = new MarkdownString('a'); + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.textContent); + }); + + test('invalid HTML', async () => { + const md = new MarkdownString('12
3
4'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('invalid HTML with attributes', async () => { + const md = new MarkdownString('14'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('valid HTML', async () => { + const md = new MarkdownString(` +

heading

+
    +
  • 1
  • +
  • hi
  • +
+
code here
`); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('mixed valid and invalid HTML', async () => { + const md = new MarkdownString(` +

heading

+
+
    +
  • 1
  • +
  • hi
  • +
+
+
canvas here
`); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('self-closing elements', async () => { + const md = new MarkdownString('

'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('html comments', async () => { + const md = new MarkdownString('
content
'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('CDATA', async () => { + const md = new MarkdownString('content]]>'); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); + + test('remote images', async () => { + const md = new MarkdownString(' '); + md.supportHtml = true; + const result = store.add(testRenderer.render(md)); + await assertSnapshot(result.element.outerHTML); + }); +}); 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 492a80f54a0..40f72512a6a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -41,14 +41,14 @@ suite('ChatVariables', function () { test('ChatVariables - resolveVariables', async function () { - const v1 = service.registerVariable({ name: 'foo', description: 'bar' }, async () => 'farboo'); - const v2 = service.registerVariable({ name: 'far', description: 'boo' }, async () => 'farboo'); + const v1 = service.registerVariable({ id: 'id', name: 'foo', description: 'bar' }, async () => 'farboo'); + const v2 = service.registerVariable({ id: 'id', name: 'far', description: 'boo' }, async () => 'farboo'); const parser = instantiationService.createInstance(ChatRequestParser); const resolveVariables = async (text: string) => { const result = parser.parseChatRequest('1', text); - return await service.resolveVariables(result, null!, () => { }, CancellationToken.None); + return await service.resolveVariables(result, undefined, null!, () => { }, CancellationToken.None); }; { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap index 11c9c2ef292..730cb160f96 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap @@ -4,7 +4,8 @@ value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newline", isTrusted: false, supportThemeIcons: false, - supportHtml: false + supportHtml: false, + baseUri: undefined }, kind: "markdownContent" } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap index bc1d5cda51e..714c3f06ed6 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap @@ -4,7 +4,8 @@ value: "some code\nover\nmultiple lines content with vuln\nand\nnewlinesmore code\nwith newlinecontent with vuln\nand\nnewlines", isTrusted: false, supportThemeIcons: false, - supportHtml: false + supportHtml: false, + baseUri: undefined }, kind: "markdownContent" } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap index 229ab7c6ac4..f68fd93b8bd 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap @@ -4,7 +4,8 @@ value: "some code content with vuln after", isTrusted: false, supportThemeIcons: false, - supportHtml: false + supportHtml: false, + baseUri: undefined }, kind: "markdownContent" } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap index 776a2546830..0a210c51cfc 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -90,6 +90,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { @@ -119,6 +120,7 @@ }, variableName: "debugConsole", variableArg: "", + variableId: "copilot.debugConsole", kind: "var" } ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap index 60d1a54726f..874558504ef 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap @@ -59,6 +59,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { @@ -88,6 +89,7 @@ }, variableName: "debugConsole", variableArg: "", + variableId: "copilot.debugConsole", kind: "var" } ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap index 17b89a36298..d826520078a 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variable_with_question_mark.0.snap @@ -27,6 +27,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap index 5af3a10d3b8..8334f7ba2b0 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_variables.0.snap @@ -27,6 +27,7 @@ }, variableName: "selection", variableArg: "", + variableId: "copilot.selection", kind: "var" }, { 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 83ff5c45284..383288306cb 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 @@ -1,7 +1,7 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, initialLocation: "panel", welcomeMessage: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap index 75fe21b2b15..a0e87754398 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap @@ -1,7 +1,7 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, initialLocation: "panel", welcomeMessage: undefined, 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 c4e365a2a54..cf9cc42dfc6 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 @@ -1,7 +1,7 @@ { requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "test", + responderUsername: "", responderAvatarIconUri: undefined, initialLocation: "panel", welcomeMessage: undefined, @@ -31,10 +31,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], - metadata: { - requester: { name: "test" }, - fullName: "test" - }, + metadata: { requester: { name: "test" } }, slashCommands: [ ] }, kind: "agent" @@ -73,10 +70,7 @@ publisherDisplayName: "", extensionDisplayName: "", locations: [ "panel" ], - metadata: { - requester: { name: "test" }, - fullName: "test" - }, + metadata: { requester: { name: "test" } }, slashCommands: [ ] }, slashCommand: undefined, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap new file mode 100644 index 00000000000..a749780f183 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -0,0 +1,81 @@ +{ + requesterUsername: "test", + requesterAvatarIconUri: undefined, + responderUsername: "", + responderAvatarIconUri: undefined, + initialLocation: "panel", + welcomeMessage: undefined, + requests: [ + { + message: { + parts: [ + { + range: { + start: 0, + endExclusive: 28 + }, + editorRange: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 29 + }, + agent: { + name: "ChatProviderWithUsedContext", + id: "ChatProviderWithUsedContext", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", + locations: [ "panel" ], + metadata: { }, + slashCommands: [ ] + }, + kind: "agent" + }, + { + range: { + start: 28, + endExclusive: 41 + }, + editorRange: { + startLineNumber: 1, + startColumn: 29, + endLineNumber: 1, + endColumn: 42 + }, + text: " test request", + kind: "text" + } + ], + text: "@ChatProviderWithUsedContext test request" + }, + variableData: { variables: [ ] }, + response: [ ], + result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, + followups: undefined, + isCanceled: false, + vote: undefined, + agent: { + name: "ChatProviderWithUsedContext", + id: "ChatProviderWithUsedContext", + extensionId: { + value: "nullExtensionDescription", + _lower: "nullextensiondescription" + }, + extensionPublisherId: "", + publisherDisplayName: "", + extensionDisplayName: "", + locations: [ "panel" ], + metadata: { }, + slashCommands: [ ] + }, + slashCommand: undefined, + usedContext: undefined, + contentReferences: [ ] + } + ] +} \ No newline at end of file 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 f611d83b246..b9f38a04549 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -121,6 +121,36 @@ suite('ChatModel', () => { model.removeRequest(requests[0].id); assert.strictEqual(model.getRequests().length, 0); }); + + test('adoptRequest', async function () { + const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Editor)); + const model2 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel)); + + model1.startInitialize(); + model1.initialize(undefined); + + model2.startInitialize(); + model2.initialize(undefined); + + const text = 'hello'; + const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); + + assert.strictEqual(model1.getRequests().length, 1); + assert.strictEqual(model2.getRequests().length, 0); + assert.ok(request1.session === model1); + assert.ok(request1.response?.session === model1); + + model2.adoptRequest(request1); + + assert.strictEqual(model1.getRequests().length, 0); + assert.strictEqual(model2.getRequests().length, 1); + assert.ok(request1.session === model2); + assert.ok(request1.response?.session === model2); + + model2.acceptResponseProgress(request1, { content: new MarkdownString('Hello'), kind: 'markdownContent' }); + + assert.strictEqual(request1.response.response.asString(), 'Hello'); + }); }); suite('Response', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index 6d4713e110d..c0b7a9542a4 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -89,6 +89,7 @@ suite('ChatRequestParser', () => { test('variables', async () => { varService.hasVariable.returns(true); + varService.getVariable.returns({ id: 'copilot.selection' }); parser = instantiationService.createInstance(ChatRequestParser); const text = 'What does #selection mean?'; @@ -98,6 +99,7 @@ suite('ChatRequestParser', () => { test('variable with question mark', async () => { varService.hasVariable.returns(true); + varService.getVariable.returns({ id: 'copilot.selection' }); parser = instantiationService.createInstance(ChatRequestParser); const text = 'What is #selection?'; @@ -184,6 +186,8 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); + varService.getVariable.onCall(0).returns({ id: 'copilot.selection' }); + varService.getVariable.onCall(1).returns({ id: 'copilot.debugConsole' }); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent /subCommand \nPlease do with #selection\nand #debugConsole'); @@ -196,6 +200,8 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); + varService.getVariable.onCall(0).returns({ id: 'copilot.selection' }); + varService.getVariable.onCall(1).returns({ id: 'copilot.debugConsole' }); parser = instantiationService.createInstance(ChatRequestParser); const result = parser.parseChatRequest('1', '@agent Please \ndo /subCommand with #selection\nand #debugConsole'); 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 bcca1f9bc98..1e9ad196f8b 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -95,7 +95,7 @@ suite('ChatService', () => { testDisposables.add(chatAgentService.registerAgent('testAgent', { name: 'testAgent', id: 'testAgent', isDefault: true, extensionId: nullExtensionDescription.identifier, extensionPublisherId: '', publisherDisplayName: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, { name: chatAgentWithUsedContextId, id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, extensionPublisherId: '', publisherDisplayName: '', extensionDisplayName: '', locations: [ChatAgentLocation.Panel], metadata: {}, slashCommands: [] })); testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent)); - chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); + chatAgentService.updateAgent('testAgent', { requester: { name: 'test' } }); }); test('retrieveSession', async () => { @@ -130,9 +130,20 @@ suite('ChatService', () => { assert.strictEqual(model.getRequests()[0].response?.response.asString(), 'test response'); }); + test('sendRequest fails', async () => { + const testService = testDisposables.add(instantiationService.createInstance(ChatService)); + + const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); + const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`); + assert(response); + await response.responseCompletePromise; + + await assertSnapshot(model.toExport()); + }); + test('can serialize', async () => { testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); - chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' }); + chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' } }); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); const model = testDisposables.add(testService.startSession(ChatAgentLocation.Panel, CancellationToken.None)); 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 9441aacda6b..314b4589e31 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts @@ -29,4 +29,15 @@ suite('ChatWordCounter', () => { cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); }); + + test('getNWords, matching links', () => { + const cases: [string, number, string][] = [ + ['[hello](https://example.com) world', 1, '[hello](https://example.com)'], + ['[hello](https://example.com) world', 2, '[hello](https://example.com) world'], + ['oh [hello](https://example.com "title") world', 1, 'oh'], + ['oh [hello](https://example.com "title") world', 2, 'oh [hello](https://example.com "title")'], + ]; + + cases.forEach(([str, nWords, result]) => doTest(str, nWords, result)); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index f2671c73f59..c7cc9199757 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -46,6 +46,9 @@ export class MockChatService implements IChatService { resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions | undefined): Promise { throw new Error('Method not implemented.'); } + adoptRequest(sessionId: string, request: IChatRequestModel): Promise { + throw new Error('Method not implemented.'); + } removeRequest(sessionid: string, requestId: string): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index 902631c259a..be7a61b525e 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -5,7 +5,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -31,12 +32,16 @@ export class MockChatVariablesService implements IChatVariablesService { return []; } - async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { return { variables: [] }; } + attachContext(name: string, value: unknown, location: ChatAgentLocation): void { + throw new Error('Method not implemented.'); + } + resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts similarity index 94% rename from src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts rename to src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index 5e94b169a06..2ff31b0173c 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -11,10 +11,11 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifec import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ProviderResult } from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgress, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; -import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; +import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChatService'; import { ISpeechProvider, ISpeechService, ISpeechToTextEvent, ISpeechToTextSession, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; @@ -66,6 +67,9 @@ suite('VoiceChat', () => { getAgent(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); } getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); } + 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.'); } } class TestSpeechService implements ISpeechService { @@ -120,7 +124,7 @@ suite('VoiceChat', () => { setup(() => { emitter = disposables.add(new Emitter()); - service = disposables.add(new VoiceChatService(new TestSpeechService(), new TestChatAgentService())); + service = disposables.add(new VoiceChatService(new TestSpeechService(), new TestChatAgentService(), new MockContextKeyService())); }); teardown(() => { 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 new file mode 100644 index 00000000000..249fd8e457c --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/electron-sandbox/voiceChatActions.test.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { parseNextChatResponseChunk } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; + +suite('VoiceChatActions', function () { + + function assertChunk(text: string, expected: string | undefined, offset: number): { chunk: string | undefined; offset: number } { + const res = parseNextChatResponseChunk(text, offset); + assert.strictEqual(res.chunk, expected); + + return res; + } + + test('parseNextChatResponseChunk', function () { + + // Simple, no offset + assertChunk('Hello World', undefined, 0); + assertChunk('Hello World.', undefined, 0); + assertChunk('Hello World. ', 'Hello World.', 0); + assertChunk('Hello World? ', 'Hello World?', 0); + assertChunk('Hello World! ', 'Hello World!', 0); + assertChunk('Hello World: ', 'Hello World:', 0); + + // Ensure chunks are parsed from the end, no offset + assertChunk('Hello World. How is your day? And more...', 'Hello World. How is your day?', 0); + + // Ensure chunks are parsed from the end, with offset + let offset = assertChunk('Hello World. How is your ', 'Hello World.', 0).offset; + offset = assertChunk('Hello World. How is your day? And more...', 'How is your day?', offset).offset; + offset = assertChunk('Hello World. How is your day? And more to come! ', 'And more to come!', offset).offset; + assertChunk('Hello World. How is your day? And more to come! ', undefined, offset); + + // Sparted by newlines + offset = assertChunk('Hello World.\nHow is your', 'Hello World.', 0).offset; + assertChunk('Hello World.\nHow is your day?\n', 'How is your day?', offset); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); +}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts index 377f64adf0a..705308f8619 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.ts @@ -11,7 +11,7 @@ import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { HasSpeechProvider, ISpeechService, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; +import { HasSpeechProvider, ISpeechService, SpeechToTextInProgress, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { Codicon } from 'vs/base/common/codicons'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { EditorAction2, EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -40,7 +40,11 @@ export class EditorDictationStartAction extends EditorAction2 { id: 'workbench.action.editorDictation.start', title: localize2('startDictation', "Start Dictation in Editor"), category: VOICE_CATEGORY, - precondition: ContextKeyExpr.and(HasSpeechProvider, EDITOR_DICTATION_IN_PROGRESS.toNegated(), EditorContextKeys.readOnly.toNegated()), + precondition: ContextKeyExpr.and( + HasSpeechProvider, + SpeechToTextInProgress.toNegated(), // disable when any speech-to-text is in progress + EditorContextKeys.readOnly.toNegated() // disable in read-only editors + ), f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyV, diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts new file mode 100644 index 00000000000..6ca47252ca5 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor/commands'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; +import { localize } from 'vs/nls'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { getCommentCommandInfo } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 105; + readonly name = 'diff-editor'; + readonly when = ContextKeyEqualsExpr.create('isInDiffEditor', true); + readonly type = AccessibleViewType.Help; + getProvider(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + const codeEditorService = accessor.get(ICodeEditorService); + const keybindingService = accessor.get(IKeybindingService); + const contextKeyService = accessor.get(IContextKeyService); + + if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { + return; + } + + const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!codeEditor) { + return; + } + + const switchSides = localize('msg3', "Run the command Diff Editor: Switch Side to toggle between the original and modified editors."); + const diffEditorActiveAnnouncement = localize('msg5', "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor."); + + const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; + const content = [ + localize('msg1', "You are in a diff editor."), + localize('msg2', "View the next or previous diff in diff review mode, which is optimized for screen readers.", AccessibleDiffViewerNext.id, AccessibleDiffViewerPrev.id), + switchSides, + diffEditorActiveAnnouncement, + localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), + ]; + const commentCommandInfo = getCommentCommandInfo(keybindingService, contextKeyService, codeEditor); + if (commentCommandInfo) { + content.push(commentCommandInfo); + } + return { + id: AccessibleViewProviderId.DiffEditor, + verbositySettingKey: AccessibilityVerbositySettingId.DiffEditor, + provideContent: () => content.join('\n\n'), + onClose: () => { + codeEditor.focus(); + }, + options: { type: AccessibleViewType.Help } + }; + } +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index b47c9bc5642..052608edd82 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -3,29 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } from 'vs/base/common/lifecycle'; import { autorunWithStore, observableFromEvent } from 'vs/base/common/observable'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { AccessibleDiffViewerNext, AccessibleDiffViewerPrev } from 'vs/editor/browser/widget/diffEditor/commands'; -import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/diffEditorWidget'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyEqualsExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; import { FloatingEditorClickWidget } from 'vs/workbench/browser/codeeditor'; import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { getCommentCommandInfo } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { DiffEditorAccessibilityHelp } from 'vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp'; class DiffEditorHelperContribution extends Disposable implements IDiffEditorContribution { public static readonly ID = 'editor.contrib.diffEditorHelper'; @@ -33,13 +25,11 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont constructor( private readonly _diffEditor: IDiffEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, @INotificationService private readonly _notificationService: INotificationService, ) { super(); - this._register(createScreenReaderHelp()); - const isEmbeddedDiffEditor = this._diffEditor instanceof EmbeddedDiffEditorWidget; if (!isEmbeddedDiffEditor) { @@ -56,7 +46,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont null )); store.add(helperWidget.onClick(() => { - this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false); + this._textResourceConfigurationService.updateValue(this._diffEditor.getModel()!.modified.uri, 'diffEditor.ignoreTrimWhitespace', false); })); helperWidget.render(); } @@ -72,7 +62,7 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont [{ label: localize('removeTimeout', "Remove Limit"), run: () => { - this._configurationService.updateValue('diffEditor.maxComputationTime', 0); + this._textResourceConfigurationService.updateValue(this._diffEditor.getModel()!.modified.uri, 'diffEditor.maxComputationTime', 0); } }], {} @@ -83,59 +73,6 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont } } -function createScreenReaderHelp(): IDisposable { - return AccessibilityHelpAction.addImplementation(105, 'diff-editor', async (accessor) => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const editorService = accessor.get(IEditorService); - const codeEditorService = accessor.get(ICodeEditorService); - const keybindingService = accessor.get(IKeybindingService); - const contextKeyService = accessor.get(IContextKeyService); - - if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { - return; - } - - const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - if (!codeEditor) { - return; - } - - const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); - let switchSides; - const switchSidesKb = keybindingService.lookupKeybinding('diffEditor.switchSide')?.getAriaLabel(); - if (switchSidesKb) { - switchSides = localize('msg3', "Run the command Diff Editor: Switch Side ({0}) to toggle between the original and modified editors.", switchSidesKb); - } else { - switchSides = localize('switchSidesNoKb', "Run the command Diff Editor: Switch Side, which is currently not triggerable via keybinding, to toggle between the original and modified editors."); - } - - const diffEditorActiveAnnouncement = localize('msg5', "The setting, accessibility.verbosity.diffEditorActive, controls if a diff editor announcement is made when it becomes the active editor."); - - const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; - const content = [ - localize('msg1', "You are in a diff editor."), - localize('msg2', "View the next ({0}) or previous ({1}) diff in diff review mode, which is optimized for screen readers.", next, previous), - switchSides, - diffEditorActiveAnnouncement, - localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), - ]; - const commentCommandInfo = getCommentCommandInfo(keybindingService, contextKeyService, codeEditor); - if (commentCommandInfo) { - content.push(commentCommandInfo); - } - accessibleViewService.show({ - id: AccessibleViewProviderId.DiffEditor, - verbositySettingKey: AccessibilityVerbositySettingId.DiffEditor, - provideContent: () => content.join('\n\n'), - onClose: () => { - codeEditor.focus(); - }, - options: { type: AccessibleViewType.Help } - }); - }, ContextKeyEqualsExpr.create('isInDiffEditor', true)); -} - registerDiffEditorContribution(DiffEditorHelperContribution.ID, DiffEditorHelperContribution); Registry.as(Extensions.ConfigurationMigration) @@ -148,3 +85,4 @@ Registry.as(Extensions.ConfigurationMigration) ]; } }]); +AccessibleViewRegistry.register(new DiffEditorAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index fab89ecfe3c..b047830a881 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -22,7 +22,6 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IContentActionHandler, renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { ApplyFileSnippetAction } from 'vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { IInlineChatService, IInlineChatSessionProvider } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -36,6 +35,7 @@ import { LOG_MODE_ID, OUTPUT_MODE_ID } from 'vs/workbench/services/output/common 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'; const $ = dom.$; @@ -76,7 +76,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { @IHoverService protected readonly hoverService: IHoverService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IInlineChatSessionService private readonly inlineChatSessionService: IInlineChatSessionService, - @IInlineChatService protected readonly inlineChatService: IInlineChatService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService protected readonly productService: IProductService, ) { @@ -84,7 +84,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { this.toDispose.push(this.editor.onDidChangeModel(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelLanguage(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelContent(() => this.update())); - this.toDispose.push(this.inlineChatService.onDidChangeProviders(() => this.update())); + this.toDispose.push(this.chatAgentService.onDidChangeAgents(() => this.update())); this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.update())); this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (e.hasChanged(EditorOption.readOnly)) { @@ -146,9 +146,9 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { return false; } - const inlineChatProviders = [...this.inlineChatService.getAllProvider()]; - const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID && !inlineChatProviders.length; - return inlineChatProviders.length > 0 || shouldRenderDefaultHint; + const hasEditorAgents = Boolean(this.chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); + const shouldRenderDefaultHint = model?.uri.scheme === Schemas.untitled && languageId === PLAINTEXT_LANGUAGE_ID && hasEditorAgents; + return hasEditorAgents || shouldRenderDefaultHint; } protected update(): void { @@ -162,7 +162,7 @@ export class EmptyTextEditorHintContribution implements IEditorContribution { this.configurationService, this.hoverService, this.keybindingService, - this.inlineChatService, + this.chatAgentService, this.telemetryService, this.productService ); @@ -195,7 +195,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { private readonly configurationService: IConfigurationService, private readonly hoverService: IHoverService, private readonly keybindingService: IKeybindingService, - private readonly inlineChatService: IInlineChatService, + private readonly chatAgentService: IChatAgentService, private readonly telemetryService: ITelemetryService, private readonly productService: IProductService ) { @@ -218,8 +218,8 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { return EmptyTextEditorHintContentWidget.ID; } - private _getHintInlineChat(providers: IInlineChatSessionProvider[]) { - const providerName = (providers.length === 1 ? providers[0].label : undefined) ?? this.productService.nameShort; + private _getHintInlineChat(providers: IChatAgent[]) { + const providerName = (providers.length === 1 ? providers[0].fullName : undefined) ?? this.productService.nameShort; const inlineChatId = 'inlineChat.start'; let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; @@ -399,7 +399,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { this.domNode.style.width = 'max-content'; this.domNode.style.paddingLeft = '4px'; - const inlineChatProviders = [...this.inlineChatService.getAllProvider()]; + const inlineChatProviders = this.chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Editor)); const { hintElement, ariaLabel } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders); this.domNode.append(hintElement); this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.EmptyEditorHint)); 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 cc9316468bc..9344f1a46a6 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -18,6 +18,10 @@ import { IRange } from 'vs/editor/common/core/range'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { execSync } from 'child_process'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { EncodedTokenizationResult, IState, ITokenizationSupport, TokenizationRegistry } from 'vs/editor/common/languages'; +import { NullState } from 'vs/editor/common/languages/nullTokenize'; +import { MetadataConsts, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; function getIRange(range: IRange): IRange { return { @@ -32,7 +36,12 @@ const enum LanguageId { TypeScript = 'ts-test' } -function registerLanguage(languageConfigurationService: ILanguageConfigurationService, languageId: LanguageId): IDisposable { +function registerLanguage(instantiationService: TestInstantiationService, languageId: LanguageId): IDisposable { + const languageService = instantiationService.get(ILanguageService) + return languageService.registerLanguage({ id: languageId }); +} + +function registerLanguageConfiguration(languageConfigurationService: ILanguageConfigurationService, languageId: LanguageId): IDisposable { let configPath: string; switch (languageId) { case LanguageId.TypeScript: @@ -47,6 +56,33 @@ function registerLanguage(languageConfigurationService: ILanguageConfigurationSe return languageConfigurationService.register(languageId, languageConfig); } +interface StandardTokenTypeData { + startIndex: number; + standardTokenType: StandardTokenType; +} + +function registerTokenizationSupport(instantiationService: TestInstantiationService, tokens: StandardTokenTypeData[][], languageId: string): IDisposable { + let lineIndex = 0; + const languageService = instantiationService.get(ILanguageService); + const tokenizationSupport: ITokenizationSupport = { + getInitialState: () => NullState, + tokenize: undefined!, + tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { + const tokensOnLine = tokens[lineIndex++]; + const encodedLanguageId = languageService.languageIdCodec.encodeLanguageId(languageId); + const result = new Uint32Array(2 * tokensOnLine.length); + for (let i = 0; i < tokensOnLine.length; i++) { + result[2 * i] = tokensOnLine[i].startIndex; + result[2 * i + 1] = + ((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) + | (tokensOnLine[i].standardTokenType << MetadataConsts.TOKEN_TYPE_OFFSET)); + } + return new EncodedTokenizationResult(result, state); + } + }; + return TokenizationRegistry.register(languageId, tokenizationSupport); +} + suite('Auto-Reindentation - TypeScript/JavaScript', () => { const languageId = LanguageId.TypeScript; @@ -59,7 +95,9 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { disposables = new DisposableStore(); instantiationService = createModelServices(disposables); languageConfigurationService = instantiationService.get(ILanguageConfigurationService); - disposables.add(registerLanguage(languageConfigurationService, languageId)); + disposables.add(instantiationService); + disposables.add(registerLanguage(instantiationService, languageId)); + disposables.add(registerLanguageConfiguration(languageConfigurationService, languageId)); }); teardown(() => { @@ -160,7 +198,28 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { 'const foo = `{`;', ' ', ].join('\n'); + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 5, standardTokenType: StandardTokenType.Other }, + { startIndex: 6, standardTokenType: StandardTokenType.Other }, + { startIndex: 9, standardTokenType: StandardTokenType.Other }, + { startIndex: 10, standardTokenType: StandardTokenType.Other }, + { startIndex: 11, standardTokenType: StandardTokenType.Other }, + { startIndex: 12, standardTokenType: StandardTokenType.String }, + { startIndex: 13, standardTokenType: StandardTokenType.String }, + { startIndex: 14, standardTokenType: StandardTokenType.String }, + { startIndex: 15, standardTokenType: StandardTokenType.Other }, + { startIndex: 16, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 4, standardTokenType: StandardTokenType.Other }] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + model.tokenization.forceTokenization(1); + model.tokenization.forceTokenization(2); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); assert.deepStrictEqual(editOperations.length, 1); const operation = editOperations[0]; @@ -258,7 +317,29 @@ suite('Auto-Reindentation - TypeScript/JavaScript', () => { 'const r = /{/;', ' ', ].join('\n'); + const tokens: StandardTokenTypeData[][] = [ + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 5, standardTokenType: StandardTokenType.Other }, + { startIndex: 6, standardTokenType: StandardTokenType.Other }, + { startIndex: 7, standardTokenType: StandardTokenType.Other }, + { startIndex: 8, standardTokenType: StandardTokenType.Other }, + { startIndex: 9, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 10, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 11, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 12, standardTokenType: StandardTokenType.RegEx }, + { startIndex: 13, standardTokenType: StandardTokenType.Other }, + { startIndex: 14, standardTokenType: StandardTokenType.Other } + ], + [ + { startIndex: 0, standardTokenType: StandardTokenType.Other }, + { startIndex: 4, standardTokenType: StandardTokenType.Other } + ] + ]; + disposables.add(registerTokenizationSupport(instantiationService, tokens, languageId)); const model = disposables.add(instantiateTextModel(instantiationService, fileContents, languageId, options)); + model.tokenization.forceTokenization(1); + model.tokenization.forceTokenization(2); const editOperations = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount()); assert.deepStrictEqual(editOperations.length, 1); const operation = editOperations[0]; diff --git a/src/vs/workbench/contrib/commands/common/commands.contribution.ts b/src/vs/workbench/contrib/commands/common/commands.contribution.ts index c4d4a5b3649..55cf296c7f2 100644 --- a/src/vs/workbench/contrib/commands/common/commands.contribution.ts +++ b/src/vs/workbench/contrib/commands/common/commands.contribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { safeStringify } from 'vs/base/common/objects'; import * as nls from 'vs/nls'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -99,14 +100,14 @@ class RunCommands extends Action2 { const cmd = args.commands[i]; - logService.debug(`runCommands: executing ${i}-th command: ${JSON.stringify(cmd)}`); + logService.debug(`runCommands: executing ${i}-th command: ${safeStringify(cmd)}`); - const r = await this._runCommand(commandService, cmd); + await this._runCommand(commandService, cmd); - logService.debug(`runCommands: executed ${i}-th command with return value: ${JSON.stringify(r)}`); + logService.debug(`runCommands: executed ${i}-th command`); } } catch (err) { - logService.debug(`runCommands: executing ${i}-th command resulted in an error: ${err instanceof Error ? err.message : JSON.stringify(err)}`); + logService.debug(`runCommands: executing ${i}-th command resulted in an error: ${err instanceof Error ? err.message : safeStringify(err)}`); notificationService.error(err); } diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts index 632eeed2e83..3580e96799b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentReply.ts +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -60,6 +60,7 @@ export class CommentReply extends Disposable { private _commentOptions: languages.CommentOptions | undefined, private _pendingComment: string | undefined, private _parentThread: ICommentThreadWidget, + focus: boolean, private _actionRunDelegate: (() => void) | null, @ICommentService private commentService: ICommentService, @IThemeService private themeService: IThemeService, @@ -75,10 +76,10 @@ export class CommentReply extends Disposable { this.commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService); this.commentEditorIsEmpty.set(!this._pendingComment); - this.initialize(); + this.initialize(focus); } - async initialize() { + async initialize(focus: boolean) { const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID); const params = JSON.stringify({ @@ -118,7 +119,7 @@ export class CommentReply extends Disposable { // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments if (hasExistingComments) { this.createReplyButton(this.commentEditor, this.form); - } else if ((this._commentThread.comments && this._commentThread.comments.length === 0) || this._pendingComment) { + } else if (focus && ((this._commentThread.comments && this._commentThread.comments.length === 0) || this._pendingComment)) { this.expandReplyArea(); } this._error = dom.append(this.form, dom.$('.validation-error.hidden')); @@ -140,12 +141,13 @@ export class CommentReply extends Disposable { public updateCommentThread(commentThread: languages.CommentThread) { const isReplying = this.commentEditor.hasTextFocus(); + const oldAndNewBothEmpty = !this._commentThread.comments?.length && !commentThread.comments?.length; if (!this._reviewThreadReplyButton) { this.createReplyButton(this.commentEditor, this.form); } - if (this._commentThread.comments && this._commentThread.comments.length === 0) { + if (this._commentThread.comments && this._commentThread.comments.length === 0 && !oldAndNewBothEmpty) { this.expandReplyArea(); } @@ -351,7 +353,10 @@ export class CommentReply extends Disposable { } private hideReplyArea() { - this.commentEditor.getDomNode()!.style.outline = ''; + const domNode = this.commentEditor.getDomNode(); + if (domNode) { + domNode.style.outline = ''; + } this.commentEditor.setValue(''); this._pendingComment = ''; this.form.classList.remove('expand'); diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 5771a5d8a59..1d20812ebe5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -63,7 +63,7 @@ export interface ICommentController { options?: CommentOptions; contextValue?: string; owner: string; - createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined): Promise; + createCommentThreadTemplate(resource: UriComponents, range: IRange | undefined, editorId?: string): Promise; updateCommentThreadTemplate(threadHandle: number, range: IRange): Promise; deleteCommentThreadMain(commentThreadId: string): void; toggleReaction(uri: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction, token: CancellationToken): Promise; @@ -97,7 +97,7 @@ export interface ICommentService { registerCommentController(uniqueOwner: string, commentControl: ICommentController): void; unregisterCommentController(uniqueOwner?: string): void; getCommentController(uniqueOwner: string): ICommentController | undefined; - createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise; + createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined, editorId?: string): Promise; updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range): Promise; getCommentMenus(uniqueOwner: string): CommentMenus; updateComments(ownerId: string, event: CommentThreadChangedEvent): void; @@ -361,14 +361,14 @@ export class CommentService extends Disposable implements ICommentService { return this._commentControls.get(uniqueOwner); } - async createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined): Promise { + async createCommentThreadTemplate(uniqueOwner: string, resource: URI, range: Range | undefined, editorId?: string): Promise { const commentController = this._commentControls.get(uniqueOwner); if (!commentController) { return; } - return commentController.createCommentThreadTemplate(resource, range); + return commentController.createCommentThreadTemplate(resource, range, editorId); } async updateCommentThreadTemplate(uniqueOwner: string, threadHandle: number, range: Range) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 43b9b5d3ff4..3c61b8bc26f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -230,7 +230,7 @@ export class CommentThreadWidget extends } } - async display(lineHeight: number) { + async display(lineHeight: number, focus: boolean) { const headHeight = Math.max(23, Math.ceil(lineHeight * 1.2)); // 23 is the value of `Math.ceil(lineHeight * 1.2)` with the default editor font size this._header.updateHeight(headHeight); @@ -238,7 +238,7 @@ export class CommentThreadWidget extends // create comment thread only when it supports reply if (this._commentThread.canReply) { - this._createCommentForm(); + this._createCommentForm(focus); } this._createAdditionalActions(); @@ -272,7 +272,7 @@ export class CommentThreadWidget extends this._commentReply.updateCanReply(); } else { if (this._commentThread.canReply) { - this._createCommentForm(); + this._createCommentForm(false); } } })); @@ -286,7 +286,7 @@ export class CommentThreadWidget extends })); } - private _createCommentForm() { + private _createCommentForm(focus: boolean) { this._commentReply = this._scopedInstantiationService.createInstance( CommentReply, this._owner, @@ -299,6 +299,7 @@ export class CommentThreadWidget extends this._commentOptions, this._pendingComment, this, + focus, this._containerDelegate.actionRunner ); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 6ce5527186e..7a6edd60c6d 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -356,13 +356,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadWidget.layout(widthInPixel); } - async display(range: IRange | undefined) { + async display(range: IRange | undefined, shouldReveal: boolean) { if (range) { this._commentGlyph = new CommentGlyphWidget(this.editor, range?.endLineNumber ?? -1); this._commentGlyph.setThreadState(this._commentThread.state); } - await this._commentThreadWidget.display(this.editor.getOption(EditorOption.lineHeight)); + await this._commentThreadWidget.display(this.editor.getOption(EditorOption.lineHeight), shouldReveal); this._disposables.add(this._commentThreadWidget.onDidResize(dimension => { this._refresh(dimension); })); @@ -371,7 +371,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } // If this is a new comment thread awaiting user input then we need to reveal it. - if (this._commentThread.canReply && this._commentThread.isTemplate && (!this._commentThread.comments || (this._commentThread.comments.length === 0))) { + if (shouldReveal) { this.reveal(); } @@ -463,10 +463,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._viewZone.afterLineNumber = currentPosition.lineNumber; } - if (!this._commentThread.comments || !this._commentThread.comments.length) { - this._commentThreadWidget.focusCommentEditor(); - } - const capture = StableEditorScrollState.capture(this.editor); this._relayout(computedLinesNumber); capture.restore(this.editor); diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index 7c2964e6ad8..4f7bfdd711d 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -25,7 +25,11 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController'; import { MarshalledCommentThreadInternal } from 'vs/workbench/common/comments'; -import { accessibleViewCurrentProviderId, accessibleViewIsShown, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { CommentsAccessibleView } from 'vs/workbench/contrib/comments/browser/commentsAccessibleView'; +import { CommentsAccessibilityHelp } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; registerAction2(class Collapse extends ViewAction { constructor() { @@ -187,3 +191,6 @@ export class UnresolvedCommentsBadge extends Disposable implements IWorkbenchCon } Registry.as(Extensions.Workbench).registerWorkbenchContribution(UnresolvedCommentsBadge, LifecyclePhase.Eventually); + +AccessibleViewRegistry.register(new CommentsAccessibleView()); +AccessibleViewRegistry.register(new CommentsAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 921d9be01b2..5ce9ac9a0ba 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -3,39 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { AccessibleViewType, IAccessibleViewContentProvider, IAccessibleViewOptions, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ctxCommentEditorFocused } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import * as nls from 'vs/nls'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import * as strings from 'vs/base/common/strings'; -import { getActiveElement } from 'vs/base/browser/dom'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; +import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; export namespace CommentAccessibilityHelpNLS { export const intro = nls.localize('intro', "The editor contains commentable range(s). Some useful commands include:"); - export const introWidget = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled ({0})."); - export const introWidgetNoKb = nls.localize('introWidgetNoKb', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus, which is currently not triggerable via keybinding."); + export const tabFocus = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus{0}", ``); export const commentCommands = nls.localize('commentCommands', "Some useful comment commands include:"); export const escape = nls.localize('escape', "- Dismiss Comment (Escape)"); - export const nextRange = nls.localize('next', "- Go to Next Commenting Range ({0})"); - export const nextRangeNoKb = nls.localize('nextNoKb', "- Go to Next Commenting Range, which is currently not triggerable via keybinding."); - export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range ({0})"); - export const previousRangeNoKb = nls.localize('previousNoKb', "- Go to Previous Commenting Range, which is currently not triggerable via keybinding."); - export const nextCommentThreadKb = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread ({0})"); - export const nextCommentThreadNoKb = nls.localize('nextCommentThreadNoKb', "- Go to Next Comment Thread, which is currently not triggerable via keybinding."); - export const previousCommentThreadKb = nls.localize('previousCommentThreadKb', "- Go to Previous Comment Thread ({0})"); - export const previousCommentThreadNoKb = nls.localize('previousCommentThreadNoKb', "- Go to Previous Comment Thread, which is currently not triggerable via keybinding."); - export const addComment = nls.localize('addComment', "- Add Comment ({0})"); - export const addCommentNoKb = nls.localize('addCommentNoKb', "- Add Comment on Current Selection, which is currently not triggerable via keybinding."); - export const submitComment = nls.localize('submitComment', "- Submit Comment ({0})"); - export const submitCommentNoKb = nls.localize('submitCommentNoKb', "- Submit Comment, accessible via tabbing, as it's currently not triggerable with a keybinding."); + export const nextRange = nls.localize('next', "- Go to Next Commenting Range{0}", ``); + export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range{0}", ``); + export const nextCommentThread = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread{0}", ``); + export const previousCommentThread = nls.localize('previousCommentThreadKb', "- Go to Previous Comment Thread{0}", ``); + export const addComment = nls.localize('addCommentNoKb', "- Add Comment on Current Selection{0}", ``); + export const submitComment = nls.localize('submitComment', "- Submit Comment{0}", ``); } export class CommentsAccessibilityHelpProvider implements IAccessibleViewContentProvider { @@ -43,44 +32,20 @@ export class CommentsAccessibilityHelpProvider implements IAccessibleViewContent verbositySettingKey: AccessibilityVerbositySettingId = AccessibilityVerbositySettingId.Comments; options: IAccessibleViewOptions = { type: AccessibleViewType.Help }; private _element: HTMLElement | undefined; - constructor( - @IKeybindingService private readonly _keybindingService: IKeybindingService - ) { - - } - private _descriptionForCommand(commandId: string, msg: string, noKbMsg: string): string { - const kb = this._keybindingService.lookupKeybinding(commandId); - if (kb) { - return strings.format(msg, kb.getAriaLabel()); - } - return strings.format(noKbMsg, commandId); - } provideContent(): string { - this._element = getActiveElement() as HTMLElement; - const content: string[] = []; - content.push(this._descriptionForCommand(ToggleTabFocusModeAction.ID, CommentAccessibilityHelpNLS.introWidget, CommentAccessibilityHelpNLS.introWidgetNoKb) + '\n'); - content.push(CommentAccessibilityHelpNLS.commentCommands); - content.push(CommentAccessibilityHelpNLS.escape); - content.push(this._descriptionForCommand(CommentCommandId.Add, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.addCommentNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.Submit, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.submitCommentNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.NextRange, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.nextRangeNoKb)); - content.push(this._descriptionForCommand(CommentCommandId.PreviousRange, CommentAccessibilityHelpNLS.previousRange, CommentAccessibilityHelpNLS.previousRangeNoKb)); - return content.join('\n'); + return [CommentAccessibilityHelpNLS.tabFocus, CommentAccessibilityHelpNLS.commentCommands, CommentAccessibilityHelpNLS.escape, CommentAccessibilityHelpNLS.addComment, CommentAccessibilityHelpNLS.submitComment, CommentAccessibilityHelpNLS.nextRange, CommentAccessibilityHelpNLS.previousRange].join('\n'); } onClose(): void { this._element?.focus(); } } -export class CommentsAccessibilityHelpContribution extends Disposable { - static ID: 'commentsAccessibilityHelpContribution'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(110, 'comments', accessor => { - const instantiationService = accessor.get(IInstantiationService); - const accessibleViewService = accessor.get(IAccessibleViewService); - accessibleViewService.show(instantiationService.createInstance(CommentsAccessibilityHelpProvider)); - return true; - }, ContextKeyExpr.or(ctxCommentEditorFocused, CommentContextKeys.commentFocused))); +export class CommentsAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 110; + readonly name = 'comments'; + readonly type = AccessibleViewType.Help; + readonly when = ContextKeyExpr.or(ctxCommentEditorFocused, CommentContextKeys.commentFocused); + getProvider(accessor: ServicesAccessor) { + return accessor.get(IInstantiationService).createInstance(CommentsAccessibilityHelpProvider); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts new file mode 100644 index 00000000000..2fdac52458d --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { MarshalledId } from 'vs/base/common/marshallingIds'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { IMenuService } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { COMMENTS_VIEW_ID, CommentsMenus } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { CommentsPanel, CONTEXT_KEY_HAS_COMMENTS } from 'vs/workbench/contrib/comments/browser/commentsView'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; + +export class CommentsAccessibleView extends Disposable implements IAccessibleViewImplentation { + readonly priority = 90; + readonly name = 'comment'; + readonly when = CONTEXT_KEY_HAS_COMMENTS; + readonly type = AccessibleViewType.View; + getProvider(accessor: ServicesAccessor) { + const contextKeyService = accessor.get(IContextKeyService); + const viewsService = accessor.get(IViewsService); + const menuService = accessor.get(IMenuService); + const commentsView = viewsService.getActiveViewWithId(COMMENTS_VIEW_ID); + if (!commentsView) { + return; + } + const menus = this._register(new CommentsMenus(menuService)); + menus.setContextKeyService(contextKeyService); + + function resolveProvider() { + if (!commentsView) { + return; + } + + const commentNode = commentsView.focusedCommentNode; + const content = commentsView.focusedCommentInfo?.toString(); + if (!commentNode || !content) { + return; + } + const menuActions = [...menus.getResourceContextActions(commentNode)].filter(i => i.enabled); + const actions = menuActions.map(action => { + return { + ...action, + run: () => { + commentsView.focus(); + action.run({ + thread: commentNode.thread, + $mid: MarshalledId.CommentThread, + commentControlHandle: commentNode.controllerHandle, + commentThreadHandle: commentNode.threadHandle, + }); + } + }; + }); + return { + id: AccessibleViewProviderId.Notification, + provideContent: () => { + return content; + }, + onClose(): void { + commentsView.focus(); + }, + next(): void { + commentsView.focus(); + commentsView.focusNextNode(); + resolveProvider(); + }, + previous(): void { + commentsView.focus(); + commentsView.focusPreviousNode(); + resolveProvider(); + }, + verbositySettingKey: AccessibilityVerbositySettingId.Comments, + options: { type: AccessibleViewType.View }, + actions + }; + } + return resolveProvider(); + } + constructor() { + super(); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 5a847e234c7..5ba6ca59e04 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -838,6 +838,40 @@ export class CommentController implements IEditorContribution { } } + private async handleCommentAdded(editorId: string | undefined, uniqueOwner: string, thread: languages.AddedCommentThread): Promise { + const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); + if (matchedZones.length) { + return; + } + + const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); + + if (matchedNewCommentThreadZones.length) { + matchedNewCommentThreadZones[0].update(thread); + return; + } + + const continueOnCommentIndex = this._inProcessContinueOnComments.get(uniqueOwner)?.findIndex(pending => { + if (pending.range === undefined) { + return thread.range === undefined; + } else { + return Range.lift(pending.range).equalsRange(thread.range); + } + }); + let continueOnCommentText: string | undefined; + if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { + continueOnCommentText = this._inProcessContinueOnComments.get(uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].body; + } + + 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)); + await this.displayCommentThread(uniqueOwner, thread, shouldReveal, pendingCommentText, pendingEdits); + this._commentInfos.filter(info => info.uniqueOwner === uniqueOwner)[0].threads.push(thread); + this.tryUpdateReservedSpace(); + } + public onModelChanged(): void { this.localToDispose.clear(); this.tryUpdateReservedSpace(); @@ -903,45 +937,17 @@ export class CommentController implements IEditorContribution { } }); - changed.forEach(thread => { + for (const thread of changed) { const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); if (matchedZones.length) { const matchedZone = matchedZones[0]; matchedZone.update(thread); this.openCommentsView(thread); } - }); + } + const editorId = this.editor?.getId(); for (const thread of added) { - const matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.threadId === thread.threadId); - if (matchedZones.length) { - return; - } - - const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.uniqueOwner === e.uniqueOwner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); - - if (matchedNewCommentThreadZones.length) { - matchedNewCommentThreadZones[0].update(thread); - return; - } - - const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.uniqueOwner)?.findIndex(pending => { - if (pending.range === undefined) { - return thread.range === undefined; - } else { - return Range.lift(pending.range).equalsRange(thread.range); - } - }); - let continueOnCommentText: string | undefined; - if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { - continueOnCommentText = this._inProcessContinueOnComments.get(e.uniqueOwner)?.splice(continueOnCommentIndex, 1)[0].body; - } - - const pendingCommentText = (this._pendingNewCommentCache[e.uniqueOwner] && this._pendingNewCommentCache[e.uniqueOwner][thread.threadId]) - ?? continueOnCommentText; - const pendingEdits = this._pendingEditsCache[e.uniqueOwner] && this._pendingEditsCache[e.uniqueOwner][thread.threadId]; - await this.displayCommentThread(e.uniqueOwner, thread, pendingCommentText, pendingEdits); - this._commentInfos.filter(info => info.uniqueOwner === e.uniqueOwner)[0].threads.push(thread); - this.tryUpdateReservedSpace(); + await this.handleCommentAdded(editorId, e.uniqueOwner, thread); } for (const thread of pending) { @@ -1020,7 +1026,7 @@ export class CommentController implements IEditorContribution { return undefined; } - private async displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): Promise { + private async displayCommentThread(uniqueOwner: string, thread: languages.CommentThread, shouldReveal: boolean, pendingComment: string | undefined, pendingEdits: { [key: number]: string } | undefined): Promise { const editor = this.editor?.getModel(); if (!editor) { return; @@ -1034,7 +1040,7 @@ export class CommentController implements IEditorContribution { continueOnCommentReply = this.commentService.removeContinueOnComment({ uniqueOwner, uri: editor.uri, range: thread.range, isReply: true }); } const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.body, pendingEdits); - await zoneWidget.display(thread.range); + await zoneWidget.display(thread.range, shouldReveal); this._commentWidgets.push(zoneWidget); this.openCommentsView(thread); } @@ -1202,7 +1208,7 @@ export class CommentController implements IEditorContribution { if (!this.editor) { return; } - this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range); + this.commentService.createCommentThreadTemplate(ownerId, this.editor.getModel()!.uri, range, this.editor.getId()); this.processNextThreadToAdd(); return; } @@ -1325,7 +1331,7 @@ export class CommentController implements IEditorContribution { pendingEdits = providerEditsCacheStore[thread.threadId]; } - await this.displayCommentThread(info.uniqueOwner, thread, pendingComment, pendingEdits); + await this.displayCommentThread(info.uniqueOwner, thread, false, pendingComment, pendingEdits); } for (const thread of info.pendingCommentThreads ?? []) { this.resumePendingComment(this.editor!.getModel()!.uri, thread); diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index f419804c8d7..896d352efe9 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -23,10 +23,11 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { accessibilityHelpIsShown, accessibleViewCurrentProviderId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibilityHelpIsShown, accessibleViewCurrentProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { CommentCommandId } from 'vs/workbench/contrib/comments/common/commentCommandIds'; import { registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions'; import { CommentsInputContentProvider } from 'vs/workbench/contrib/comments/browser/commentsInputContentProvider'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; registerEditorContribution(ID, CommentController, EditorContributionInstantiation.AfterFirstRender); registerWorkbenchContribution2(CommentsInputContentProvider.ID, CommentsInputContentProvider, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 685db8b3b31..a4d5c4e74e2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -259,7 +259,7 @@ export class StartDebugActionViewItem extends BaseActionViewItem { }); }); - this.selectBox.setOptions(this.debugOptions.map((data, index) => { text: data.label, isDisabled: disabledIdxs.indexOf(index) !== -1 }), this.selected); + this.selectBox.setOptions(this.debugOptions.map((data, index): ISelectOptionItem => ({ text: data.label, isDisabled: disabledIdxs.indexOf(index) !== -1 })), this.selected); } } @@ -314,7 +314,7 @@ export class FocusSessionActionViewItem extends SelectActionViewItem { text: data }), session ? sessions.indexOf(session) : undefined); + this.setOptions(names.map((data): ISelectOptionItem => ({ text: data })), session ? sessions.indexOf(session) : undefined); } private getSelectedSession(): IDebugSession | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index c13e82fd187..4ab2ba9d777 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -25,11 +25,12 @@ import { Constants } from 'vs/base/common/uint'; import { URI } from 'vs/base/common/uri'; import { CoreEditingCommands } from 'vs/editor/browser/coreCommands'; import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { IEditorHoverOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IEditorHoverOptions } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { DEFAULT_WORD_REGEXP } from 'vs/editor/common/core/wordHelper'; +import { ScrollType } from 'vs/editor/common/editorCommon'; import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { InlineValue, InlineValueContext } from 'vs/editor/common/languages'; import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops } from 'vs/editor/common/model'; @@ -641,6 +642,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { return new RunOnceScheduler( () => { this.displayedStore.clear(); + this.oldDecorations.clear(); }, 100 ); @@ -803,9 +805,26 @@ export class DebugEditorContribution implements IDebugEditorContribution { decoration => `${decoration.range.startLineNumber}:${decoration?.options.after?.content}`); } - if (!cts.token.isCancellationRequested) { - this.oldDecorations.set(allDecorations); - this.displayedStore.add(toDisposable(() => this.oldDecorations.clear())); + if (cts.token.isCancellationRequested) { + return; + } + + // If word wrap is on, application of inline decorations may change the scroll position. + // Ensure the cursor maintains its vertical position relative to the viewport when + // we apply decorations. + let preservePosition: { position: Position; top: number } | undefined; + if (this.editor.getOption(EditorOption.wordWrap) !== 'off') { + const position = this.editor.getPosition(); + if (position && this.editor.getVisibleRanges().some(r => r.containsPosition(position))) { + preservePosition = { position, top: this.editor.getTopForPosition(position.lineNumber, position.column) }; + } + } + + this.oldDecorations.set(allDecorations); + + if (preservePosition) { + const top = this.editor.getTopForPosition(preservePosition.position.lineNumber, preservePosition.position.column); + this.editor.setScrollTop(this.editor.getScrollTop() - (preservePosition.top - top), ScrollType.Immediate); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index c5db527c526..d09eada1d2a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -42,7 +42,7 @@ import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { DebugTaskRunner, TaskRunResult } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, DEBUG_MEMORY_SCHEME, DEBUG_SCHEME, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, debuggerDisabledMessage, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IFunctionBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry'; @@ -796,8 +796,6 @@ export class DebugService implements IDebugService { if (launch) { unresolved = launch.getConfiguration(session.configuration.name); if (unresolved && !equals(unresolved, session.unresolvedConfiguration)) { - // Take the type from the session since the debug extension might overwrite it #21316 - unresolved.type = session.configuration.type; unresolved.noDebug = session.configuration.noDebug; needsToSubstitute = true; } @@ -811,7 +809,7 @@ export class DebugService implements IDebugService { if (resolvedByProviders) { resolved = await this.substituteVariables(launch, resolvedByProviders); if (resolved && !initCancellationToken.token.isCancellationRequested) { - resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, unresolved.type, resolved, initCancellationToken.token); + resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, resolved.type, resolved, initCancellationToken.token); } } else { resolved = resolvedByProviders; @@ -1073,8 +1071,14 @@ export class DebugService implements IDebugService { return this.sendAllBreakpoints(); } - addFunctionBreakpoint(name?: string, id?: string, mode?: string): void { - this.model.addFunctionBreakpoint(name || '', id, mode); + async addFunctionBreakpoint(opts?: IFunctionBreakpointOptions, id?: string): Promise { + this.model.addFunctionBreakpoint(opts ?? { name: '' }, id); + // If opts not provided, sending the breakpoint is handled by a later to call to `updateFunctionBreakpoint` + if (opts) { + this.debugStorage.storeBreakpoints(this.model); + await this.sendFunctionBreakpoints(); + this.debugStorage.storeBreakpoints(this.model); + } } async updateFunctionBreakpoint(id: string, update: { name?: string; hitCondition?: string; condition?: string }): Promise { diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 8c6ec806b3a..7369a367607 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -421,7 +421,7 @@ class WatchExpressionsDragAndDrop implements ITreeDragAndDrop { } } - return { accept: true, effect: { type: ListDragOverEffectType.Move, position: dropEffectPosition }, feedback: [targetIndex] } as ITreeDragOverReaction; + return { accept: true, effect: { type: ListDragOverEffectType.Move, position: dropEffectPosition }, feedback: [targetIndex] } satisfies ITreeDragOverReaction; } getDragURI(element: IExpression): string | null { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 4b8445dc43d..e48607b9eda 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -24,7 +24,7 @@ import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorPane } from 'vs/workbench/common/editor'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { IDataBreakpointOptions, IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; +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 { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -1150,7 +1150,7 @@ export interface IDebugService { /** * Adds a new function breakpoint for the given name. */ - addFunctionBreakpoint(name?: string, id?: string, mode?: string): void; + addFunctionBreakpoint(opts?: IFunctionBreakpointOptions, id?: string): void; /** * Updates an already existing function breakpoint. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index b12e9b726ff..13f7d1d902d 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 } from 'vs/workbench/contrib/debug/common/debug'; +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 { 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'; @@ -579,9 +579,9 @@ export class Thread implements IThread { getTopStackFrame(): IStackFrame | undefined { const callStack = this.getCallStack(); // Allow stack frame without source and with instructionReferencePointer as top stack frame when using disassembly view. - const firstAvailableStackFrame = callStack.find(sf => !!(sf && + const firstAvailableStackFrame = callStack.find(sf => !!( ((this.stoppedDetails?.reason === 'instruction breakpoint' || (this.stoppedDetails?.reason === 'step' && this.lastSteppingGranularity === 'instruction')) && sf.instructionPointerReference) || - (sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'))); + (sf.source && sf.source.available && !isFrameDeemphasized(sf)))); return firstAvailableStackFrame; } @@ -1896,8 +1896,8 @@ export class DebugModel extends Disposable implements IDebugModel { this._onDidChangeBreakpoints.fire({ changed: changed, sessionOnly: false }); } - addFunctionBreakpoint(functionName: string, id?: string, mode?: string): IFunctionBreakpoint { - const newFunctionBreakpoint = new FunctionBreakpoint({ name: functionName, mode }, id); + addFunctionBreakpoint(opts: IFunctionBreakpointOptions, id?: string): IFunctionBreakpoint { + const newFunctionBreakpoint = new FunctionBreakpoint(opts, id); this.functionBreakpoints.push(newFunctionBreakpoint); this._onDidChangeBreakpoints.fire({ added: [newFunctionBreakpoint], sessionOnly: false }); 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 61599c36ce9..6b47aa1dc3f 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -161,8 +161,8 @@ suite('Debug - Breakpoints', () => { }); test('function breakpoints', () => { - model.addFunctionBreakpoint('foo', '1'); - model.addFunctionBreakpoint('bar', '2'); + model.addFunctionBreakpoint({ name: 'foo' }, '1'); + model.addFunctionBreakpoint({ name: 'bar' }, '2'); model.updateFunctionBreakpoint('1', { name: 'fooUpdated' }); model.updateFunctionBreakpoint('2', { name: 'barUpdated' }); @@ -380,7 +380,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(result.message, 'Data Breakpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-data'); - const functionBreakpoint = model.addFunctionBreakpoint('foo', '1'); + const functionBreakpoint = model.addFunctionBreakpoint({ name: 'foo' }, '1'); result = getBreakpointMessageAndIcon(State.Stopped, true, functionBreakpoint, ls, model); assert.strictEqual(result.message, 'Function Breakpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-function'); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts index b22715ac423..42680a4ba1f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts @@ -13,7 +13,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { localize } from 'vs/nls'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -240,7 +239,7 @@ export class ExtensionFeaturesTab extends Themable { multipleSelectionSupport: false, setRowLineHeight: false, horizontalScrolling: false, - accessibilityProvider: >{ + accessibilityProvider: { getAriaLabel(extensionFeature: IExtensionFeatureDescriptor | null): string { return extensionFeature?.label ?? ''; }, diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index b38717d2a21..92a43009c52 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -6,9 +6,9 @@ import { localize, localize2 } from 'vs/nls'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, extensionsConfigurationNodeBase } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -275,13 +275,13 @@ CommandsRegistry.registerCommand('_extensions.manage', (accessor: ServicesAccess } }); -CommandsRegistry.registerCommand('extension.open', async (accessor: ServicesAccessor, extensionId: string, tab?: ExtensionEditorTab, preserveFocus?: boolean, feature?: string) => { +CommandsRegistry.registerCommand('extension.open', async (accessor: ServicesAccessor, extensionId: string, tab?: ExtensionEditorTab, preserveFocus?: boolean, feature?: string, sideByside?: boolean) => { const extensionService = accessor.get(IExtensionsWorkbenchService); const commandService = accessor.get(ICommandService); const [extension] = await extensionService.getExtensions([{ id: extensionId }], CancellationToken.None); if (extension) { - return extensionService.open(extension, { tab, preserveFocus, feature }); + return extensionService.open(extension, { tab, preserveFocus, feature, sideByside }); } return commandService.executeCommand('_extensions.manage', extensionId, tab, preserveFocus, feature); @@ -365,13 +365,13 @@ CommandsRegistry.registerCommand({ isMachineScoped: options?.donotSync ? true : undefined, /* do not allow syncing extensions automatically while installing through the command */ installPreReleaseVersion: options?.installPreReleaseVersion, installGivenVersion: !!version, - context: options?.context + context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.COMMAND }, }); } else { await extensionsWorkbenchService.install(arg, { version, installPreReleaseVersion: options?.installPreReleaseVersion, - context: options?.context, + context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.COMMAND }, justification: options?.justification, enable: options?.enable, isMachineScoped: options?.donotSync ? true : undefined, /* do not allow syncing extensions automatically while installing through the command */ @@ -635,7 +635,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); const autoUpdateExtensionsSubMenu = new MenuId('autoUpdateExtensionsSubMenu'); - MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, { + 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), @@ -930,7 +930,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); const extensionsFilterSubMenu = new MenuId('extensionsFilterSubMenu'); - MenuRegistry.appendMenuItem(extensionsSearchActionsMenu, { + MenuRegistry.appendMenuItem(extensionsSearchActionsMenu, { submenu: extensionsFilterSubMenu, title: localize('filterExtensions', "Filter Extensions..."), group: 'navigation', @@ -1016,7 +1016,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); const extensionsCategoryFilterSubMenu = new MenuId('extensionsCategoryFilterSubMenu'); - MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { submenu: extensionsCategoryFilterSubMenu, title: localize('filter by category', "Category"), when: CONTEXT_HAS_GALLERY, @@ -1129,7 +1129,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); const extensionsSortSubMenu = new MenuId('extensionsSortSubMenu'); - MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { + MenuRegistry.appendMenuItem(extensionsFilterSubMenu, { submenu: extensionsSortSubMenu, title: localize('sorty by', "Sort By"), when: ContextKeyExpr.and(ContextKeyExpr.or(CONTEXT_HAS_GALLERY, DefaultViewsContext)), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 1fd2e6152b3..a2f80109939 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -39,7 +39,7 @@ import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/w import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IQuickPickItem, IQuickInputService, IQuickPickSeparator, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPickItem, IQuickInputService, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -1666,8 +1666,8 @@ function getQuickPickEntries(themes: IWorkbenchTheme[], currentTheme: IWorkbench } } if (showCurrentTheme) { - picks.push({ type: 'separator', label: localize('current', "current") }); - picks.push({ label: currentTheme.label, id: currentTheme.id }); + picks.push({ type: 'separator', label: localize('current', "current") }); + picks.push({ label: currentTheme.label, id: currentTheme.id }); } return picks; } @@ -2086,7 +2086,7 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio .then(reference => { const position = reference.object.textEditorModel.getPositionAt(offset); reference.dispose(); - return { + return { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, @@ -2667,7 +2667,7 @@ export class ReinstallAction extends Action { label: extension.displayName, description: extension.identifier.id, extension, - } as (IQuickPickItem & { extension: IExtension }); + }; }); return entries; }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts index 33683a48388..2fa59067e8b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewer.ts @@ -23,7 +23,7 @@ import { listFocusForeground, listFocusBackground, foreground, editorBackground import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { IListAccessibilityProvider, IListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IStyleOverride } from 'vs/platform/theme/browser/defaultStyles'; import { getAriaLabelForExtension } from 'vs/workbench/contrib/extensions/browser/extensionsViews'; @@ -264,7 +264,7 @@ export class ExtensionsTree extends WorkbenchAsyncDataTree>{ + accessibilityProvider: { getAriaLabel(extensionData: IExtensionData): string { return getAriaLabelForExtension(extensionData.extension); }, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 63d1feb8d16..4dcb55bb040 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -45,7 +45,6 @@ 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'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { isVirtualWorkspace } from 'vs/platform/workspace/common/virtualWorkspace'; @@ -207,7 +206,7 @@ export class ExtensionsListView extends ViewPane { multipleSelectionSupport: false, setRowLineHeight: false, horizontalScrolling: false, - accessibilityProvider: >{ + accessibilityProvider: { getAriaLabel(extension: IExtension | null): string { return getAriaLabelForExtension(extension); }, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index e94ab96d100..3d725460a20 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -553,7 +553,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { if (this.extension) { this.hover.value = this.hoverService.setupUpdatableHover({ delay: this.configurationService.getValue('workbench.hover.delay'), - showHover: (options) => { + showHover: (options, focus) => { return this.hoverService.showHover({ ...options, additionalClasses: ['extension-hover'], @@ -561,7 +561,10 @@ export class ExtensionHoverWidget extends ExtensionWidget { hoverPosition: this.options.position(), forcePosition: true, }, - }); + persistence: { + hideOnKeyDown: true, + } + }, focus); }, placement: 'element' }, this.options.target, { markdown: () => Promise.resolve(this.getHoverMarkdown()), markdownNotSupportedFallback: undefined }); diff --git a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts index 3ad131168e5..613fe523715 100644 --- a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts @@ -20,7 +20,7 @@ export class KeymapRecommendations extends ExtensionRecommendations { protected async doActivate(): Promise { if (this.productService.keymapExtensionTips) { - this._recommendations = this.productService.keymapExtensionTips.map(extensionId => ({ + this._recommendations = this.productService.keymapExtensionTips.map(extensionId => ({ extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, diff --git a/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts index ac493c927fe..a310c9f4fdb 100644 --- a/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/languageRecommendations.ts @@ -20,7 +20,7 @@ export class LanguageRecommendations extends ExtensionRecommendations { protected async doActivate(): Promise { if (this.productService.languageExtensionTips) { - this._recommendations = this.productService.languageExtensionTips.map(extensionId => ({ + this._recommendations = this.productService.languageExtensionTips.map((extensionId): ExtensionRecommendation => ({ extension: extensionId.toLowerCase(), reason: { reasonId: ExtensionRecommendationReason.Application, @@ -29,6 +29,4 @@ export class LanguageRecommendations extends ExtensionRecommendations { })); } } - } - diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index b2eaa27034a..7bca4243703 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -44,7 +44,7 @@ color: var(--vscode-extensionButton-foreground) !important; } -.monaco-action-bar .action-item .action-label.extension-action.label:hover { +.monaco-action-bar .action-item:not(.disabled) .action-label.extension-action.label:hover { background-color: var(--vscode-extensionButton-hoverBackground) !important; } @@ -61,7 +61,7 @@ color: var(--vscode-extensionButton-prominentForeground) !important; } -.monaco-action-bar .action-item .action-label.extension-action.label.prominent:hover { +.monaco-action-bar .action-item.action-item:not(.disabled) .action-label.extension-action.label.prominent:hover { background-color: var(--vscode-extensionButton-prominentHoverBackground); } diff --git a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts index 2e6373fe684..6ca5f7d48f0 100644 --- a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts @@ -26,6 +26,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { Registry } from 'vs/platform/registry/common/platform'; import { IExternalTerminalConfiguration, IExternalTerminalService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const OPEN_IN_TERMINAL_COMMAND_ID = 'openInTerminal'; const OPEN_IN_INTEGRATED_TERMINAL_COMMAND_ID = 'openInIntegratedTerminal'; @@ -47,7 +48,7 @@ function registerOpenTerminalCommand(id: string, explorerKind: 'integrated' | 'e } catch { } - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IExplorerService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); return fileService.resolveAll(resources.map(r => ({ resource: r }))).then(async stats => { // Always use integrated terminal when using a remote const config = configurationService.getValue(); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 88b9f76da0c..963e5c56925 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -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 } from 'vs/workbench/common/contextkeys'; +import { DirtyWorkingCopiesContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, WorkbenchStateContext, WorkspaceFolderCountContext, SidebarFocusContext, ActiveEditorCanRevertContext, ActiveEditorContext, ResourceContextKey, ActiveEditorAvailableEditorIdsContext, MultipleEditorsSelectedInGroupContext, TwoEditorsSelectedInGroupContext } 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'; @@ -155,18 +155,19 @@ const copyRelativePathCommand = { }; // Editor Title Context Menu -appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, copyPathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste'); -appendEditorTitleContextMenuItem(COPY_RELATIVE_PATH_COMMAND_ID, copyRelativePathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste'); -appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Explorer View"), ResourceContextKey.IsFileSystemResource, '2_files', 1); +appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, copyPathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste', true); +appendEditorTitleContextMenuItem(COPY_RELATIVE_PATH_COMMAND_ID, copyRelativePathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste', true); +appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Explorer View"), ResourceContextKey.IsFileSystemResource, '2_files', false, 1); -export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group: string, order?: number): void { +export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group: string, supportsMultiSelect: boolean, order?: number): void { + const precondition = supportsMultiSelect !== true ? MultipleEditorsSelectedInGroupContext.negate() : undefined; // Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { - command: { id, title }, + command: { id, title, precondition }, when, group, - order + order, }); } @@ -415,6 +416,13 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { when: ContextKeyExpr.and(ResourceContextKey.HasResource, WorkbenchListDoubleSelection, isFileOrUntitledResourceContextKey) }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { + group: '3_compare', + order: 30, + command: compareSelectedCommand, + when: ContextKeyExpr.and(ResourceContextKey.HasResource, TwoEditorsSelectedInGroupContext, isFileOrUntitledResourceContextKey) +}); + MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '4_close', order: 10, diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index da1e9c4dcd5..81bf68c3e0b 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -90,10 +90,11 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }, id: OPEN_TO_SIDE_COMMAND_ID, handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); + const editorGroupService = accessor.get(IEditorGroupsService); const listService = accessor.get(IListService); const fileService = accessor.get(IFileService); const explorerService = accessor.get(IExplorerService); - const resources = getMultiSelectedResources(resource, listService, editorService, explorerService); + const resources = getMultiSelectedResources(resource, listService, editorService, editorGroupService, explorerService); // Set side input if (resources.length) { @@ -203,8 +204,9 @@ CommandsRegistry.registerCommand({ id: COMPARE_SELECTED_COMMAND_ID, handler: async (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); + const editorGroupService = accessor.get(IEditorGroupsService); const explorerService = accessor.get(IExplorerService); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, explorerService); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, editorGroupService, explorerService); if (resources.length === 2) { return editorService.openEditor({ @@ -253,7 +255,7 @@ async function resourcesToClipboard(resources: URI[], relative: boolean, clipboa } const copyPathCommandHandler: ICommandHandler = async (accessor, resource: URI | object) => { - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); await resourcesToClipboard(resources, false, accessor.get(IClipboardService), accessor.get(ILabelService), accessor.get(IConfigurationService)); }; @@ -280,7 +282,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); const copyRelativePathCommandHandler: ICommandHandler = async (accessor, resource: URI | object) => { - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); await resourcesToClipboard(resources, true, accessor.get(IClipboardService), accessor.get(ILabelService), accessor.get(IConfigurationService)); }; @@ -571,7 +573,7 @@ CommandsRegistry.registerCommand({ const contextService = accessor.get(IWorkspaceContextService); const uriIdentityService = accessor.get(IUriIdentityService); const workspace = contextService.getWorkspace(); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)).filter(resource => + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)).filter(resource => workspace.folders.some(folder => uriIdentityService.extUri.isEqual(folder.uri, resource)) // Need to verify resources are workspaces since multi selection can trigger this command on some non workspace resources ); diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index b3e8260dde4..f1dbd239f4e 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -105,7 +105,7 @@ export function getResourceForCommand(resource: URI | object | undefined, listSe return EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); } -export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService, explorerService: IExplorerService): Array { +export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService, editorGroupService: IEditorGroupsService, explorerService: IExplorerService): Array { const list = listService.lastFocusedList; const element = list?.getHTMLElement(); if (element && isActiveElement(element)) { @@ -137,6 +137,17 @@ export function getMultiSelectedResources(resource: URI | object | undefined, li } } + // Check for tabs multiselect. + const activeGroup = editorGroupService.activeGroup; + const selection = activeGroup.selectedEditors; + if (selection.length > 1 && URI.isUri(resource)) { + // If the resource is part of the tabs selection, return all selected tabs/resources. + // It's possible that multiple tabs are selected but the action was applied to a resource that is not part of the selection. + if (selection.some(e => e.matches({ resource }))) { + return selection.map(editor => EditorResourceAccessor.getOriginalUri(editor)).filter(uri => !!uri); + } + } + const result = getResourceForCommand(resource, listService, editorService); return !!result ? [result] : []; } diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 3fb7600cc0f..7a3237b22f5 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -9,15 +9,6 @@ height: 100%; } -.explorer-item-hover { - /* -- Must set important as hover overrides the cursor -- */ - cursor: pointer !important; - padding-left: 6px; - height: 22px; - font-size: 13px !important; - user-select: none !important; -} - .explorer-folders-view .monaco-list-row { padding-left: 4px; /* align top level twistie with `Explorer` title label */ } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 62e9f635c48..068fde51b80 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -48,7 +48,7 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree' import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ILabelService } from 'vs/platform/label/common/label'; -import { isNumber, isStringArray } from 'vs/base/common/types'; +import { isNumber } from 'vs/base/common/types'; import { IEditableData } from 'vs/workbench/common/views'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -62,13 +62,9 @@ import { ResourceSet } from 'vs/base/common/map'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { timeout } from 'vs/base/common/async'; -import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/hover/hoverDelegate'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; -import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { mainWindow } from 'vs/base/browser/window'; import { IExplorerFileContribution, explorerFileContribRegistry } from 'vs/workbench/contrib/files/browser/explorerFileContrib'; -import type { IHoverWidget } from 'vs/base/browser/ui/hover/hover'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -283,81 +279,6 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event; - private readonly hoverDelegate = new class implements IHoverDelegate { - - private lastHoverHideTime = 0; - private hiddenFromClick = false; - readonly placement = 'element'; - - get delay() { - // Delay implementation borrowed froms src/vs/workbench/browser/parts/statusbar/statusbarPart.ts - if (Date.now() - this.lastHoverHideTime < 500) { - return 0; // show instantly when a hover was recently shown - } - - return this.configurationService.getValue('workbench.hover.delay'); - } - - constructor( - private readonly configurationService: IConfigurationService, - private readonly hoverService: IHoverService - ) { } - - showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined { - let element: HTMLElement; - if (options.target instanceof HTMLElement) { - element = options.target; - } else { - element = options.target.targetElements[0]; - } - - const tlRow = element.closest('.monaco-tl-row') as HTMLElement | undefined; - const listRow = tlRow?.closest('.monaco-list-row') as HTMLElement | undefined; - - const child = element.querySelector('div.monaco-icon-label-container') as Element | undefined; - const childOfChild = child?.querySelector('span.monaco-icon-name-container') as HTMLElement | undefined; - let overflowed = false; - if (childOfChild && child) { - const width = child.clientWidth; - const childWidth = childOfChild.offsetWidth; - // Check if element is overflowing its parent container - overflowed = width <= childWidth; - } - - // Only count decorations that provide additional info, as hover overing decorations such as git excluded isn't helpful - const hasDecoration = options.content.toString().includes('•'); - // If it's overflowing or has a decoration show the tooltip - overflowed = overflowed || hasDecoration; - - const indentGuideElement = tlRow?.querySelector('.monaco-tl-indent') as HTMLElement | undefined; - if (!indentGuideElement) { - return; - } - - return overflowed ? this.hoverService.showHover({ - ...options, - target: indentGuideElement, - container: listRow, - additionalClasses: ['explorer-item-hover'], - position: { - hoverPosition: HoverPosition.RIGHT, - }, - appearance: { - compact: true, - skipFadeInAnimation: true, - showPointer: false, - } - }, focus) : undefined; - } - - onDidHideHover(): void { - if (!this.hiddenFromClick) { - this.lastHoverHideTime = Date.now(); - } - this.hiddenFromClick = false; - } - }(this.configurationService, this.hoverService); - constructor( container: HTMLElement, private labels: ResourceLabels, @@ -369,7 +290,6 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); @@ -402,8 +322,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer('explorer.experimental.hover'); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true, hoverDelegate: experimentalHover ? this.hoverDelegate : undefined })); + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); templateDisposables.add(label.onDidRender(() => { try { if (templateData.currentContext) { @@ -524,11 +443,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer('explorer.experimental.hover'); templateData.contribs.forEach(c => c.setResource(stat.resource)); templateData.label.setResource({ resource: stat.resource, name: label }, { - title: experimentalHover ? isStringArray(label) ? label[0] : label : undefined, fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE, extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses, fileDecorations: this.config.explorer.decorations, diff --git a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts index de8140c144b..8cb362e422c 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts @@ -22,6 +22,7 @@ import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { appendToCommandPalette, appendEditorTitleContextMenuItem } from 'vs/workbench/contrib/files/browser/fileActions.contribution'; import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS'; const REVEAL_IN_OS_LABEL = isWindows ? nls.localize2('revealInWindows', "Reveal in File Explorer") : isMacintosh ? nls.localize2('revealInMac', "Reveal in Finder") : nls.localize2('openContainer', "Open Containing Folder"); @@ -36,7 +37,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyR }, handler: (accessor: ServicesAccessor, resource: URI | object) => { - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); revealResourcesInOS(resources, accessor.get(INativeHostService), accessor.get(IWorkspaceContextService)); } }); @@ -57,7 +58,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL.value, REVEAL_IN_OS_WHEN_CONTEXT, '2_files', 0); +appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL.value, REVEAL_IN_OS_WHEN_CONTEXT, '2_files', false, 0); // Menu registration - open editors diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index ca4254400e7..4339d4117f9 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -37,7 +37,7 @@ import { generateUuid } from 'vs/base/common/uuid'; type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; -class DefaultFormatter extends Disposable implements IWorkbenchContribution { +export class DefaultFormatter extends Disposable implements IWorkbenchContribution { static readonly configName = 'editor.defaultFormatter'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index d2399af6b7b..4ab19a40890 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -7,28 +7,27 @@ import { EditorContributionInstantiation, registerEditorContribution } from 'vs/ import { 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 { IInlineChatService, INLINE_CHAT_ID, INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { INLINE_CHAT_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { InlineChatNotebookContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatNotebook'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from 'vs/workbench/common/contributions'; import { InlineChatSavingServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl'; -import { InlineChatAccessibleViewContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView'; +import { InlineChatAccessibleView } from 'vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView'; import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; +import { InlineChatEnabler, InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; // --- browser -registerSingleton(IInlineChatService, InlineChatServiceImpl, InstantiationType.Delayed); -registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Eager); // EAGER because this registers an agent which we need swiftly +registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed); registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors -registerEditorContribution(INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID, InlineChatActions.InlineAccessibilityHelpContribution, EditorContributionInstantiation.Eventually); + registerAction2(InlineChatActions.StartSessionAction); registerAction2(InlineChatActions.CloseAction); @@ -36,8 +35,7 @@ registerAction2(InlineChatActions.ConfigureInlineChatAction); registerAction2(InlineChatActions.UnstashSessionAction); registerAction2(InlineChatActions.DiscardHunkAction); registerAction2(InlineChatActions.DiscardAction); -registerAction2(InlineChatActions.DiscardToClipboardAction); -registerAction2(InlineChatActions.DiscardUndoToNewFileAction); +registerAction2(InlineChatActions.RerunAction); registerAction2(InlineChatActions.CancelSessionAction); registerAction2(InlineChatActions.MoveToNextHunk); registerAction2(InlineChatActions.MoveToPreviousHunk); @@ -54,4 +52,7 @@ registerAction2(InlineChatActions.CopyRecordings); const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); -workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatAccessibleViewContribution, LifecyclePhase.Eventually); + +registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); + +AccessibleViewRegistry.register(new InlineChatAccessibleView()); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts new file mode 100644 index 00000000000..d168f54f836 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.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 { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { getChatAccessibilityHelpProvider } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; +import { CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; + +export class InlineChatAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 106; + readonly name = 'inlineChat'; + readonly type = AccessibleViewType.Help; + readonly when = ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED); + getProvider(accessor: ServicesAccessor) { + const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); + if (!codeEditor) { + return; + } + return getChatAccessibilityHelpProvider(accessor, codeEditor, 'inlineChat'); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts index fbb0e1b2012..4cac306c23f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts @@ -5,44 +5,41 @@ import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +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'; -export class InlineChatAccessibleViewContribution extends Disposable { - static ID: 'inlineChatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(100, 'inlineChat', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const codeEditorService = accessor.get(ICodeEditorService); +export class InlineChatAccessibleView implements IAccessibleViewImplentation { + readonly priority = 100; + readonly name = 'inlineChat'; + readonly when = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED); + readonly type = AccessibleViewType.View; + getProvider(accessor: ServicesAccessor) { + const codeEditorService = accessor.get(ICodeEditorService); - const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); - if (!editor) { - return false; - } - const controller = InlineChatController.get(editor); - if (!controller) { - return false; - } - const responseContent = controller?.getMessage(); - if (!responseContent) { - return false; - } - accessibleViewService.show({ - id: AccessibleViewProviderId.InlineChat, - verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, - provideContent(): string { return responseContent; }, - onClose() { - controller.focus(); - }, - - options: { type: AccessibleViewType.View } - }); - return true; - }, ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); + if (!editor) { + return; + } + const controller = InlineChatController.get(editor); + if (!controller) { + return; + } + const responseContent = controller?.getMessage(); + if (!responseContent) { + return; + } + return { + id: AccessibleViewProviderId.InlineChat, + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + options: { type: AccessibleViewType.View } + }; } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 521edfebb17..ce5b514ede3 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,27 +11,25 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, 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_RESPONSE_FOCUSED, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_HAS_STASHED_SESSION, ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET, ACTION_TOGGLE_DIFF, ACTION_REGENERATE_RESPONSE } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { localize, localize2 } from 'vs/nls'; -import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { fromNow } from 'vs/base/common/date'; import { IInlineChatSessionService, Recording } from './inlineChatSessionService'; -import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; -import { Disposable } from 'vs/base/common/lifecycle'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; 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); @@ -252,18 +250,6 @@ export class DiscardHunkAction extends AbstractInlineChatAction { } } - -MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, { - submenu: MENU_INLINE_CHAT_WIDGET_DISCARD, - title: localize('discardMenu', "Discard..."), - icon: Codicon.discard, - group: '0_main', - order: 2, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Live), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.OnlyMessages)), - rememberDefaultAction: true -}); - - export class DiscardAction extends AbstractInlineChatAction { constructor() { @@ -276,11 +262,6 @@ export class DiscardAction extends AbstractInlineChatAction { weight: KeybindingWeight.EditorContrib - 1, primary: KeyCode.Escape, when: CTX_INLINE_CHAT_USER_DID_EDIT.negate() - }, - menu: { - id: MENU_INLINE_CHAT_WIDGET_DISCARD, - group: '0_main', - order: 0 } }); } @@ -290,60 +271,6 @@ export class DiscardAction extends AbstractInlineChatAction { } } -export class DiscardToClipboardAction extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineChat.discardToClipboard', - title: localize('undo.clipboard', 'Discard to Clipboard'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_DID_EDIT), - // keybinding: { - // weight: KeybindingWeight.EditorContrib + 10, - // primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ, - // mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyZ }, - // }, - menu: { - id: MENU_INLINE_CHAT_WIDGET_DISCARD, - group: '0_main', - order: 1 - } - }); - } - - override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController): Promise { - const clipboardService = accessor.get(IClipboardService); - const changedText = await ctrl.cancelSession(); - if (changedText !== undefined) { - clipboardService.writeText(changedText); - } - } -} - -export class DiscardUndoToNewFileAction extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineChat.discardToFile', - title: localize('undo.newfile', 'Discard to New File'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_DID_EDIT), - menu: { - id: MENU_INLINE_CHAT_WIDGET_DISCARD, - group: '0_main', - order: 2 - } - }); - } - - override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: any[]): Promise { - const editorService = accessor.get(IEditorService); - const changedText = await ctrl.cancelSession(); - if (changedText !== undefined) { - const input: IUntitledTextResourceEditorInput = { forceUntitled: true, resource: undefined, contents: changedText, languageId: editor.getModel()?.getLanguageId() }; - editorService.openEditor(input, SIDE_GROUP); - } - } -} - export class ToggleDiffForChange extends AbstractInlineChatAction { constructor() { @@ -359,7 +286,8 @@ export class ToggleDiffForChange extends AbstractInlineChatAction { { 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) + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), + order: 10, } ] }); @@ -555,27 +483,40 @@ export class ViewInChatAction extends AbstractInlineChatAction { icon: Codicon.commentDiscussion, precondition: CTX_INLINE_CHAT_VISIBLE, menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - when: CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), - group: '0_main', - order: 1 + id: MENU_INLINE_CHAT_WIDGET, + group: 'navigation', + order: 5 } }); } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.viewInChat(); + override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]) { + return ctrl.viewInChat(); } } -export class InlineAccessibilityHelpContribution extends Disposable { +export class RerunAction extends AbstractInlineChatAction { constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(106, 'inlineChat', async accessor => { - const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - if (!codeEditor) { - return; + super({ + id: ACTION_REGENERATE_RESPONSE, + title: localize2('chat.rerun.label', "Rerun Request"), + f1: false, + icon: Codicon.refresh, + precondition: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), + menu: { + id: MENU_INLINE_CHAT_WIDGET_STATUS, + group: '0_main', + order: 5, } - runAccessibilityHelpAction(accessor, codeEditor, 'inlineChat'); - }, ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED))); + }); + } + + 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 }); + } } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts index d5ac64ea3f1..b68ae908369 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -22,6 +22,7 @@ import { Range } from 'vs/editor/common/core/range'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ScrollType } from 'vs/editor/common/editorCommon'; export class InlineChatContentWidget implements IContentWidget { @@ -56,7 +57,8 @@ export class InlineChatContentWidget implements IContentWidget { new ServiceCollection([ IContextKeyService, this._store.add(contextKeyService.createScoped(this._domNode)) - ]) + ]), + this._store ); this._widget = scopedInstaService.createInstance( @@ -167,7 +169,7 @@ export class InlineChatContentWidget implements IContentWidget { this._visible = true; this._focusNext = true; - this._editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position)); + this._editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position), ScrollType.Immediate); this._widget.inputEditor.setValue(''); const wordInfo = this._editor.getModel()?.getWordAtPosition(position); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 121ee198eb4..e5794444295 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as aria from 'vs/base/browser/ui/aria/aria'; -import { Barrier, DeferredPromise, Queue, raceCancellation } from 'vs/base/common/async'; +import { Barrier, DeferredPromise, Queue } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -29,8 +29,6 @@ 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 { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { chatAgentLeader, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; 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'; @@ -38,17 +36,14 @@ import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { StashedSession } from './inlineChatSession'; import { IModelDeltaDecoration, ITextModel, 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 { tail } from 'vs/base/common/arrays'; -import { IChatRequestModel, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatRequestModel, 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 { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { isEqual } from 'vs/base/common/resources'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; @@ -122,8 +117,13 @@ export class InlineChatController implements IEditorContribution { private readonly _onWillStartSession = this._store.add(new Emitter()); readonly onWillStartSession = this._onWillStartSession.event; - readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); - readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); + get chatWidget() { + if (this._input.value.isVisible) { + return this._input.value.chatWidget; + } else { + return this._zone.value.widget.chatWidget; + } + } private readonly _sessionStore = this._store.add(new DisposableStore()); private readonly _stashedSession = this._store.add(new MutableDisposable()); @@ -140,9 +140,7 @@ export class InlineChatController implements IEditorContribution { @IConfigurationService private readonly _configurationService: IConfigurationService, @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IChatService private readonly _chatService: IChatService, - @ICommandService private readonly _commandService: ICommandService, @ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, ) { @@ -425,19 +423,19 @@ export class InlineChatController implements IEditorContribution { }); } else if (e.kind === 'removeRequest') { // TODO@jrieken this currently is buggy when removing not the very last request/response - if (this._session!.lastExchange?.response instanceof ReplyResponse) { - try { - this._session!.hunkData.ignoreTextModelNChanges = true; - await this._strategy!.undoChanges(this._session!.lastExchange.response.modelAltVersionId); - } finally { - this._session!.hunkData.ignoreTextModelNChanges = false; - } - } + // if (this._session!.lastExchange?.response instanceof ReplyResponse) { + // try { + // this._session!.hunkData.ignoreTextModelNChanges = true; + // await this._strategy!.undoChanges(this._session!.lastExchange.response.modelAltVersionId); + // } finally { + // this._session!.hunkData.ignoreTextModelNChanges = false; + // } + // } } })); // Update context key - this._ctxSupportIssueReporting.set(this._session.provider.supportIssueReporting ?? false); + this._ctxSupportIssueReporting.set(this._session.agent.metadata.supportIssueReporting ?? false); // #region DEBT // DEBT@jrieken @@ -459,13 +457,12 @@ export class InlineChatController implements IEditorContribution { const result: CompletionList = { suggestions: [], incomplete: false }; for (const command of this._session.session.slashCommands) { - const withSlash = `/${command.command}`; + const withSlash = `/${command.name}`; result.suggestions.push({ - label: { label: withSlash, description: command.detail ?? '' }, + label: { label: withSlash, description: command.description ?? '' }, kind: CompletionItemKind.Text, insertText: withSlash, range: Range.fromPositions(new Position(1, 1), position), - command: command.executeImmediately ? { id: 'workbench.action.chat.acceptInput', title: withSlash } : undefined }); } @@ -476,8 +473,8 @@ export class InlineChatController implements IEditorContribution { const updateSlashDecorations = (collection: IEditorDecorationsCollection, model: ITextModel) => { const newDecorations: IModelDeltaDecoration[] = []; - for (const command of (this._session?.session.slashCommands ?? []).sort((a, b) => b.command.length - a.command.length)) { - const withSlash = `/${command.command}`; + for (const command of (this._session?.session.slashCommands ?? []).sort((a, b) => b.name.length - a.name.length)) { + const withSlash = `/${command.name}`; const firstLine = model.getLineContent(1); if (firstLine.startsWith(withSlash)) { newDecorations.push({ @@ -493,13 +490,13 @@ export class InlineChatController implements IEditorContribution { }); // inject detail when otherwise empty - if (firstLine.trim() === `/${command.command}`) { + 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.detail}`, + content: `${command.description}`, inlineClassName: 'inline-chat-slash-command-detail' } } @@ -596,55 +593,18 @@ export class InlineChatController implements IEditorContribution { const input = request.message.text; this._zone.value.widget.value = input; - // slash command referring - let slashCommandLike = request.message.parts.find(part => part instanceof ChatRequestAgentSubcommandPart || part instanceof ChatRequestSlashCommandPart); - const refer = this._session.session.slashCommands?.some(value => { - if (value.refer) { - if (slashCommandLike?.text === `/${value.command}`) { - return true; - } - if (request?.message.text.startsWith(`/${value.command}`)) { - slashCommandLike = new ChatRequestSlashCommandPart(new OffsetRange(0, 1), new Range(1, 1, 1, 1), { command: value.command, detail: value.detail ?? '' }); - return true; - } - } - return false; - }); - if (refer && slashCommandLike && !this._session.lastExchange) { - this._log('[IE] seeing refer command, continuing outside editor', this._session.provider.extensionId); - - // cancel this request - this._chatService.cancelCurrentRequestForSession(request.session.sessionId); - - this._editor.setSelection(this._session.wholeRange.value); - let massagedInput = input; - const withoutSubCommandLeader = slashCommandLike.text.slice(1); - for (const agent of this._chatAgentService.getActivatedAgents()) { - if (agent.locations.includes(ChatAgentLocation.Panel)) { - const commands = agent.slashCommands; - if (commands.find((command) => withoutSubCommandLeader.startsWith(command.name))) { - massagedInput = `${chatAgentLeader}${agent.name} ${slashCommandLike.text}`; - break; - } - } - } - // if agent has a refer command, massage the input to include the agent name - await this._instaService.invokeFunction(sendRequest, massagedInput); - - return State.ACCEPT; - } - - this._session.addInput(new SessionPrompt(input)); + this._session.addInput(new SessionPrompt(request)); return State.SHOW_REQUEST; } - private async [State.SHOW_REQUEST](): Promise { + private async [State.SHOW_REQUEST](): Promise { assertType(this._session); assertType(this._session.chatModel.requestInProgress); - const request: IChatRequestModel | undefined = tail(this._session.chatModel.getRequests()); + const { chatModel } = this._session; + const request: IChatRequestModel | undefined = chatModel.getRequests().at(-1); assertType(request); assertType(request.response); @@ -664,21 +624,34 @@ export class InlineChatController implements IEditorContribution { const progressiveEditsClock = StopWatch.create(); const progressiveEditsQueue = new Queue(); - let lastLength = 0; + let next: State.SHOW_RESPONSE | State.CANCEL | State.PAUSE | State.ACCEPT | State.WAIT_FOR_INPUT = State.SHOW_RESPONSE; + store.add(Event.once(this._messages.event)(message => { + this._log('state=_makeRequest) message received', message); + this._chatService.cancelCurrentRequestForSession(chatModel.sessionId); + if (message & Message.CANCEL_SESSION) { + next = State.CANCEL; + } else if (message & Message.PAUSE_SESSION) { + next = State.PAUSE; + } else if (message & Message.ACCEPT_SESSION) { + next = State.ACCEPT; + } + })); - - let message = Message.NONE; - store.add(Event.once(this._messages.event)(m => { - this._log('state=_makeRequest) message received', m); - this._chatService.cancelCurrentRequestForSession(request.session.sessionId); - message = m; + store.add(chatModel.onDidChange(e => { + if (e.kind === 'removeRequest' && e.requestId === request.id) { + progressiveEditsCts.cancel(); + responsePromise.complete(); + next = State.CANCEL; + } })); // cancel the request when the user types store.add(this._zone.value.widget.chatWidget.inputEditor.onDidChangeModelContent(() => { - this._chatService.cancelCurrentRequestForSession(request.session.sessionId); + this._chatService.cancelCurrentRequestForSession(chatModel.sessionId); })); + let lastLength = 0; + let isFirstChange = true; // apply edits store.add(response.onDidChange(() => { @@ -694,13 +667,6 @@ export class InlineChatController implements IEditorContribution { return; } - // if ("1") { - // return; - // } - - // TODO@jrieken - const editsShouldBeInstant = false; - const edits = response.response.value.map(part => { if (part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)) { return part.edits; @@ -727,10 +693,12 @@ export class InlineChatController implements IEditorContribution { // influence the time it takes to receive the changes and progressive typing will // become infinitely fast for (const edits of newEdits) { - await this._makeChanges(edits, editsShouldBeInstant - ? undefined - : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } - ); + await this._makeChanges(edits, { + duration: progressiveEditsAvgDuration.value, + token: progressiveEditsCts.token + }, isFirstChange); + + isFirstChange = false; } // reshow the widget if the start position changed or shows at the wrong position @@ -755,16 +723,9 @@ export class InlineChatController implements IEditorContribution { await this._session.hunkData.recompute(); this._zone.value.widget.updateToolbar(true); + this._zone.value.widget.updateProgress(false); - if (message & Message.CANCEL_SESSION) { - return State.CANCEL; - } else if (message & Message.PAUSE_SESSION) { - return State.PAUSE; - } else if (message & Message.ACCEPT_SESSION) { - return State.ACCEPT; - } else { - return State.SHOW_RESPONSE; - } + return next; } private async[State.SHOW_RESPONSE](): Promise { @@ -810,31 +771,6 @@ export class InlineChatController implements IEditorContribution { this._zone.value.widget.updateToolbar(true); newPosition = await this._strategy.renderChanges(response); - - if (this._session.provider.provideFollowups) { - const followupCts = new CancellationTokenSource(); - const msgListener = Event.once(this._messages.event)(() => { - followupCts.cancel(); - }); - const followupTask = this._session.provider.provideFollowups(this._session.session, response.raw, followupCts.token); - this._log('followup request started', this._session.provider.extensionId, this._session.session, response.raw); - raceCancellation(Promise.resolve(followupTask), followupCts.token).then(followupReply => { - if (followupReply && this._session) { - this._log('followup request received', this._session.provider.extensionId, this._session.session, followupReply); - this._zone.value.widget.updateFollowUps(followupReply, followup => { - if (followup.kind === 'reply') { - this.updateInput(followup.message); - this.acceptInput(); - } else { - this._commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); - } - }); - } - }).finally(() => { - msgListener.dispose(); - followupCts.dispose(); - }); - } } this._showWidget(false, newPosition); @@ -976,12 +912,12 @@ export class InlineChatController implements IEditorContribution { } } - private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { + private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined, undoStopBefore: boolean) { assertType(this._session); assertType(this._strategy); const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this._session.textModelN.uri, edits); - this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.provider.extensionId, edits, moreMinimalEdits); + this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.agent.extensionId, edits, moreMinimalEdits); if (moreMinimalEdits?.length === 0) { // nothing left to do @@ -999,9 +935,9 @@ export class InlineChatController implements IEditorContribution { this._inlineChatSavingService.markChanged(this._session); this._session.wholeRange.trackEdits(editOperations); if (opts) { - await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts); + await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore); } else { - await this._strategy.makeChanges(editOperations, editsObserver); + await this._strategy.makeChanges(editOperations, editsObserver, undoStopBefore); } this._ctxDidEdit.set(this._session.hasChangedText); @@ -1024,22 +960,8 @@ export class InlineChatController implements IEditorContribution { this._zone.value.widget.updateStatus(status, { classes: ['warn'] }); } - setPlaceholder(text: string): void { - this._forcedPlaceholder = text; - this._updatePlaceholder(); - } - - resetPlaceholder(): void { - this._forcedPlaceholder = undefined; - this._updatePlaceholder(); - } - - acceptInput(): void { - if (this._input.value.isVisible) { - this._input.value.chatWidget.acceptInput(); - } else { - this._zone.value.widget.chatWidget.acceptInput(); - } + acceptInput() { + return this.chatWidget.acceptInput(); } updateInput(text: string, selectAll = true): void { @@ -1053,12 +975,6 @@ export class InlineChatController implements IEditorContribution { } } - getInput(): string { - return this._input.value.isVisible - ? this._input.value.value - : this._zone.value.widget.value; - } - cancelCurrentRequest(): void { this._messages.fire(Message.CANCEL_INPUT | Message.CANCEL_REQUEST); } @@ -1086,11 +1002,21 @@ export class InlineChatController implements IEditorContribution { this._strategy?.move?.(next); } - - viewInChat() { - if (this._session?.lastExchange?.response instanceof ReplyResponse) { - this._instaService.invokeFunction(showMessageResponse, this._session.lastExchange.prompt.value, this._session.lastExchange.response.mdContent.value); + async viewInChat() { + if (!this._strategy || !this._session) { + return; } + + // TODO@jrieken REMOVE this as soon as we can mark responses as accepted + // and as soon as hunks support request-linking + const textEditsResponseCount = this._session.chatModel.getRequests().filter(request => request.response?.response.value.some(part => part.kind === 'textEditGroup')).length; + if (textEditsResponseCount > 1) { + return; + } + + this._strategy.cancel(); + await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel); + this.cancelSession(); } toggleDiff() { @@ -1107,7 +1033,7 @@ export class InlineChatController implements IEditorContribution { if (this._session?.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { const response = this._session?.lastExchange?.response.chatResponse; this._chatService.notifyUserAction({ - sessionId: response.session.sessionId, + sessionId: this._session.chatModel.sessionId, requestId: response.requestId, agentId: response.agent?.id, result: response.result, @@ -1139,7 +1065,7 @@ export class InlineChatController implements IEditorContribution { if (this._session.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { const response = this._session?.lastExchange?.response.chatResponse; this._chatService.notifyUserAction({ - sessionId: response.session.sessionId, + sessionId: this._session.chatModel.sessionId, requestId: response.requestId, agentId: response.agent?.id, result: response.result, @@ -1180,35 +1106,21 @@ export class InlineChatController implements IEditorContribution { } } -async function showMessageResponse(accessor: ServicesAccessor, query: string, response: string) { - const chatService = accessor.get(IChatService); - const chatAgentService = accessor.get(IChatAgentService); - const agent = chatAgentService.getActivatedAgents().find(agent => agent.locations.includes(ChatAgentLocation.Panel) && agent.isDefault); - if (!agent) { - return; - } +async function moveToPanelChat(accessor: ServicesAccessor, model: ChatModel | undefined) { - const widget = await showChatView(accessor.get(IViewsService)); - if (widget && widget.viewModel) { - chatService.addCompleteRequest(widget.viewModel.sessionId, query, undefined, 0, { message: response }); + const viewsService = accessor.get(IViewsService); + const chatService = accessor.get(IChatService); + + const widget = await showChatView(viewsService); + + if (widget && widget.viewModel && model) { + for (const request of model.getRequests().slice()) { + await chatService.adoptRequest(widget.viewModel.model.sessionId, request); + } widget.focusLastMessage(); } } -async function sendRequest(accessor: ServicesAccessor, query: string) { - const chatAgentService = accessor.get(IChatAgentService); - const agent = chatAgentService.getActivatedAgents().find(agent => agent.locations.includes(ChatAgentLocation.Panel) && agent.isDefault); - if (!agent) { - return; - } - const widget = await showChatView(accessor.get(IViewsService)); - if (!widget) { - return; - } - widget.focusInput(); - widget.acceptInput(query); -} - function asInlineChatResponseType(response: IResponse): InlineChatResponseTypes { let result: InlineChatResponseTypes | undefined; for (const item of response.value) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index fb03e5a7f08..2bebeaede34 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { IWorkspaceTextEdit, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model'; -import { EditMode, IInlineChatSessionProvider, IInlineChatSession, IInlineChatBulkEditResponse, IInlineChatEditResponse, InlineChatResponseType, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode, IInlineChatSession, IInlineChatBulkEditResponse, IInlineChatEditResponse, InlineChatResponseType, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -32,8 +32,9 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ILogService } from 'vs/platform/log/common/log'; -import { ChatModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, IChatRequestModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IChatAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; export type TelemetryData = { @@ -160,7 +161,7 @@ export class Session { * The document into which AI edits went, when live this is `targetUri` otherwise it is a temporary document */ readonly textModelN: ITextModel, - readonly provider: IInlineChatSessionProvider, + readonly agent: IChatAgent, readonly session: IInlineChatSession, readonly wholeRange: SessionWholeRange, readonly hunkData: HunkData, @@ -168,7 +169,7 @@ export class Session { ) { this.textModelNAltVersion = textModelN.getAlternativeVersionId(); this._teldata = { - extension: ExtensionIdentifier.toKey(provider.extensionId), + extension: ExtensionIdentifier.toKey(agent.extensionId), startTime: this._startTime.toISOString(), endTime: this._startTime.toISOString(), edits: 0, @@ -279,9 +280,13 @@ export class Session { export class SessionPrompt { + readonly value: string; + constructor( - readonly value: string, - ) { } + readonly request: IChatRequestModel + ) { + this.value = request.message.text; + } } export class SessionExchange { @@ -450,6 +455,12 @@ export class StashedSession { // --- +function lineRangeAsRange(lineRange: LineRange, model: ITextModel): Range { + return lineRange.isEmpty + ? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, model.getLineLength(lineRange.startLineNumber)) + : new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, model.getLineLength(lineRange.endLineNumberExclusive - 1)); +} + export class HunkData { private static readonly _HUNK_TRACKED_RANGE = ModelDecorationOptions.register({ @@ -636,8 +647,8 @@ export class HunkData { const textModelNDecorations: string[] = []; const textModel0Decorations: string[] = []; - textModelNDecorations.push(accessorN.addDecoration(LineRange.asRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE)); - textModel0Decorations.push(accessor0.addDecoration(LineRange.asRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE)); + textModelNDecorations.push(accessorN.addDecoration(lineRangeAsRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE)); + textModel0Decorations.push(accessor0.addDecoration(lineRangeAsRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE)); for (const change of hunk.changes) { textModelNDecorations.push(accessorN.addDecoration(change.modifiedRange, HunkData._HUNK_TRACKED_RANGE)); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 83a2621d73a..e76cd173882 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -2,216 +2,36 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesceInPlace, isNonEmptyArray } from 'vs/base/common/arrays'; -import { raceCancellation } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Iterable } from 'vs/base/common/iterator'; -import { DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { LRUCache } from 'vs/base/common/map'; +import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; -import { ITextModel, IValidEditOperation } from 'vs/editor/common/model'; +import { IValidEditOperation } from 'vs/editor/common/model'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; -import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatFollowup, IChatProgress, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; -import { EditMode, IInlineChatBulkEditResponse, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, IInlineChatService, IInlineChatSession, IInlineChatSessionProvider, IInlineChatSlashCommand, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatBulkEditResponse, IInlineChatSession, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { EmptyResponse, ErrorResponse, HunkData, ReplyResponse, Session, SessionExchange, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession'; import { IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer, Recording } from './inlineChatSessionService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ISelection } from 'vs/editor/common/core/selection'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { Codicon } from 'vs/base/common/codicons'; -import { isEqual } from 'vs/base/common/resources'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -class BridgeAgent implements IChatAgentImplementation { - - constructor( - private readonly _data: IChatAgentData, - private readonly _sessions: ReadonlyMap, - private readonly _postLastResponse: (data: { id: string; response: ReplyResponse | ErrorResponse | EmptyResponse }) => void, - @IInstantiationService private readonly _instaService: IInstantiationService, - ) { } - - - private _findSessionDataByRequest(request: IChatAgentRequest) { - let data: SessionData | undefined; - for (const candidate of this._sessions.values()) { - if (candidate.session.chatModel.sessionId === request.sessionId) { - data = candidate; - break; - } - } - return data; - } - - async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, _history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - - if (token.isCancellationRequested) { - return {}; - } - - const data = this._findSessionDataByRequest(request); - - if (!data) { - throw new Error('FAILED to find session'); - } - - const { session } = data; - - if (!session.lastInput) { - throw new Error('FAILED to find last input'); - } - - const inlineChatContextValue = request.variables.variables.find(candidate => candidate.name === _inlineChatContext)?.value; - const inlineChatContext = typeof inlineChatContextValue === 'string' && JSON.parse(inlineChatContextValue); - - const modelAltVersionIdNow = session.textModelN.getAlternativeVersionId(); - const progressEdits: TextEdit[][] = []; - - const inlineRequest: IInlineChatRequest = { - requestId: request.requestId, - prompt: request.message, - attempt: request.attempt ?? 0, - withIntentDetection: request.enableCommandDetection ?? true, - live: session.editMode !== EditMode.Preview, - previewDocument: session.textModelN.uri, - selection: inlineChatContext.selection, - wholeRange: inlineChatContext.wholeRange - }; - - const inlineProgress = new Progress(data => { - // TODO@jrieken - // if (data.message) { - // progress({ kind: 'progressMessage', content: new MarkdownString(data.message) }); - // } - // TODO@ulugbekna,jrieken should we only send data.slashCommand when having detected one? - if (data.slashCommand && !inlineRequest.prompt.startsWith('/')) { - const command = this._data.slashCommands.find(c => c.name === data.slashCommand); - progress({ kind: 'agentDetection', agentId: this._data.id, command }); - } - if (data.markdownFragment) { - progress({ kind: 'markdownContent', content: new MarkdownString(data.markdownFragment) }); - } - if (isNonEmptyArray(data.edits)) { - progressEdits.push(data.edits); - progress({ kind: 'textEdit', uri: session.textModelN.uri, edits: data.edits }); - } - }); - - let result: IInlineChatResponse | undefined | null; - let response: ReplyResponse | ErrorResponse | EmptyResponse; - - try { - result = await data.session.provider.provideResponse(session.session, inlineRequest, inlineProgress, token); - - if (result) { - if (result.message) { - inlineProgress.report({ markdownFragment: result.message.value }); - } - if (Array.isArray(result.edits)) { - inlineProgress.report({ edits: result.edits }); - } - - const markdownContents = result.message ?? new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - - const chatModelRequest = session.chatModel.getRequests().find(candidate => candidate.id === request.requestId); - - response = this._instaService.createInstance(ReplyResponse, result, markdownContents, session.textModelN.uri, modelAltVersionIdNow, progressEdits, request.requestId, chatModelRequest?.response); - - } else { - response = new EmptyResponse(); - } - - } catch (e) { - response = new ErrorResponse(e); - } - - this._postLastResponse({ id: request.requestId, response }); - - - return { - metadata: { - inlineChatResponse: result - } - }; - } - - async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - - if (!result.metadata?.inlineChatResponse) { - return []; - } - - const data = this._findSessionDataByRequest(request); - if (!data) { - return []; - } - - const inlineFollowups = await data.session.provider.provideFollowups?.(data.session.session, result.metadata?.inlineChatResponse, token); - if (!inlineFollowups) { - return []; - } - - const chatFollowups = inlineFollowups.map(f => { - if (f.kind === 'reply') { - return { - kind: 'reply', - message: f.message, - agentId: request.agentId, - title: f.title, - tooltip: f.tooltip, - } satisfies IChatFollowup; - } else { - // TODO@jrieken update API - return undefined; - } - }); - - coalesceInPlace(chatFollowups); - return chatFollowups; - } - - provideWelcomeMessage(location: ChatAgentLocation, token: CancellationToken): string[] { - // without this provideSampleQuestions is not called - return []; - } - - async provideSampleQuestions(location: ChatAgentLocation, token: CancellationToken): Promise { - // TODO@jrieken DEBT - // (hack) this function is called while creating the session. We need the timeout to make sure this._sessions is populated. - // (hack) we have no context/session id and therefore use the first session with an active editor - await new Promise(resolve => setTimeout(resolve, 10)); - - for (const [, data] of this._sessions) { - if (data.session.session.input && data.editor.hasWidgetFocus()) { - return [{ - kind: 'reply', - agentId: _bridgeAgentId, - message: data.session.session.input, - }]; - } - } - return []; - } -} type SessionData = { editor: ICodeEditor; @@ -227,7 +47,6 @@ export class InlineChatError extends Error { } } -const _bridgeAgentId = 'brigde.editor'; const _inlineChatContext = '_inlineChatContext'; const _inlineChatDocument = '_inlineChatDocument'; @@ -264,10 +83,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { private readonly _keyComputers = new Map(); private _recordings: Recording[] = []; - private readonly _lastResponsesFromBridgeAgent = new LRUCache(5); constructor( - @IInlineChatService private readonly _inlineChatService: IInlineChatService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IModelService private readonly _modelService: IModelService, @ITextModelService private readonly _textModelService: ITextModelService, @@ -280,108 +97,11 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @IChatVariablesService chatVariableService: IChatVariablesService, ) { - const fakeProviders = this._store.add(new DisposableMap()); - - this._store.add(this._chatAgentService.onDidChangeAgents(() => { - - const providersNow = new Set(); - - for (const agent of this._chatAgentService.getActivatedAgents()) { - if (agent.id === _bridgeAgentId) { - // not interesting - continue; - } - if (!agent.locations.includes(ChatAgentLocation.Editor) || !agent.isDefault) { - // not interesting - continue; - } - providersNow.add(agent.id); - - if (!fakeProviders.has(agent.id)) { - fakeProviders.set(agent.id, _inlineChatService.addProvider(_instaService.createInstance(AgentInlineChatProvider, agent))); - this._logService.debug(`ADDED inline chat provider for agent ${agent.id}`); - } - } - - for (const [id] of fakeProviders) { - if (!providersNow.has(id)) { - fakeProviders.deleteAndDispose(id); - this._logService.debug(`REMOVED inline chat provider for agent ${id}`); - } - } - })); - - // MARK: register fake chat agent - const addOrRemoveBridgeAgent = () => { - const that = this; - const agentData: IChatAgentData = { - id: _bridgeAgentId, - name: 'editor', - extensionId: nullExtensionDescription.identifier, - publisherDisplayName: '', - extensionDisplayName: '', - extensionPublisherId: '', - isDefault: true, - locations: [ChatAgentLocation.Editor], - get slashCommands(): IChatAgentCommand[] { - // HACK@jrieken - // find the active session and return its slash commands - let candidate: Session | undefined; - for (const data of that._sessions.values()) { - if (data.editor.hasWidgetFocus()) { - candidate = data.session; - break; - } - } - if (!candidate || !candidate.session.slashCommands) { - return []; - } - return candidate.session.slashCommands.map(c => { - return { - name: c.command, - description: c.detail ?? '', - } satisfies IChatAgentCommand; - }); - }, - defaultImplicitVariables: [_inlineChatContext], - metadata: { - isSticky: false, - themeIcon: Codicon.copilot, - }, - }; - - let otherEditorAgent: IChatAgentData | undefined; - let myEditorAgent: IChatAgentData | undefined; - - for (const candidate of this._chatAgentService.getActivatedAgents()) { - if (!myEditorAgent && candidate.id === agentData.id) { - myEditorAgent = candidate; - } else if (!otherEditorAgent && candidate.isDefault && candidate.locations.includes(ChatAgentLocation.Editor)) { - otherEditorAgent = candidate; - } - } - - if (otherEditorAgent) { - bridgeStore.clear(); - _logService.debug(`REMOVED bridge agent "${agentData.id}", found "${otherEditorAgent.id}"`); - - } else if (!myEditorAgent) { - bridgeStore.value = this._chatAgentService.registerDynamicAgent(agentData, this._instaService.createInstance(BridgeAgent, agentData, this._sessions, data => { - this._lastResponsesFromBridgeAgent.set(data.id, data.response); - })); - _logService.debug(`ADDED bridge agent "${agentData.id}"`); - } - }; - - this._store.add(this._chatAgentService.onDidChangeAgents(() => addOrRemoveBridgeAgent())); - const bridgeStore = this._store.add(new MutableDisposable()); - addOrRemoveBridgeAgent(); - // MARK: implicit variable for editor selection and (tracked) whole range this._store.add(chatVariableService.registerVariable( - { name: _inlineChatContext, description: '', hidden: true }, + { id: _inlineChatContext, name: _inlineChatContext, description: '', hidden: true }, async (_message, _arg, model) => { for (const [, data] of this._sessions) { if (data.session.chatModel === model) { @@ -392,7 +112,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } )); this._store.add(chatVariableService.registerVariable( - { name: _inlineChatDocument, description: '', hidden: true }, + { id: _inlineChatDocument, name: _inlineChatDocument, description: '', hidden: true }, async (_message, _arg, model) => { for (const [, data] of this._sessions) { if (data.session.chatModel === model) { @@ -414,47 +134,27 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { async createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; wholeRange?: Range }, token: CancellationToken): Promise { const agent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Editor); - let provider: IInlineChatSessionProvider | undefined; - if (agent) { - for (const candidate of this._inlineChatService.getAllProvider()) { - if (candidate instanceof AgentInlineChatProvider && candidate.agent === agent) { - provider = candidate; - break; - } - } - } - if (!provider) { - provider = Iterable.first(this._inlineChatService.getAllProvider()); - } - - if (!provider) { - this._logService.trace('[IE] NO provider found'); + if (!agent) { + this._logService.trace('[IE] NO agent found'); return undefined; } + this._onWillStartSession.fire(editor); const textModel = editor.getModel(); const selection = editor.getSelection(); - let rawSession: IInlineChatSession | undefined | null; - try { - rawSession = await raceCancellation( - Promise.resolve(provider.prepareInlineChatSession(textModel, selection, token)), - token - ); - } catch (error) { - this._logService.error('[IE] FAILED to prepare session', provider.extensionId); - this._logService.error(error); - throw new InlineChatError((error as Error)?.message || 'Failed to prepare session'); - } - if (!rawSession) { - this._logService.trace('[IE] NO session', provider.extensionId); - return undefined; - } + + const rawSession: IInlineChatSession = { + id: Math.random(), + wholeRange: new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn), + placeholder: agent.description, + slashCommands: agent.slashCommands + }; const store = new DisposableStore(); - this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${provider.extensionId}`); + this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`); const chatModel = this._chatService.startSession(ChatAgentLocation.Editor, token); if (!chatModel) { @@ -486,59 +186,52 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { lastResponseListener.clear(); // ONCE let inlineResponse: ErrorResponse | EmptyResponse | ReplyResponse; - if (response.agent?.id === _bridgeAgentId) { - // use result that was provided by - inlineResponse = this._lastResponsesFromBridgeAgent.get(response.requestId) ?? new ErrorResponse(new Error('Missing Response')); - this._lastResponsesFromBridgeAgent.delete(response.requestId); + // make an response from the ChatResponseModel + if (response.isCanceled) { + // error: cancelled + inlineResponse = new ErrorResponse(new CancellationError()); + } else if (response.result?.errorDetails) { + // error: "real" error + inlineResponse = new ErrorResponse(new Error(response.result.errorDetails.message)); + } else if (response.response.value.length === 0) { + // epmty response + inlineResponse = new EmptyResponse(); } else { - // make an artificial response from the ChatResponseModel - if (response.isCanceled) { - // error: cancelled - inlineResponse = new ErrorResponse(new CancellationError()); - } else if (response.result?.errorDetails) { - // error: "real" error - inlineResponse = new ErrorResponse(new Error(response.result.errorDetails.message)); - } else if (response.response.value.length === 0) { - // epmty response - inlineResponse = new EmptyResponse(); - } else { - // replay response - const markdownContent = new MarkdownString(); - const raw: IInlineChatBulkEditResponse = { - id: Math.random(), - type: InlineChatResponseType.BulkEdit, - message: markdownContent, - edits: { edits: [] }, - }; - for (const item of response.response.value) { - if (item.kind === 'markdownContent') { - markdownContent.value += item.content.value; - } else if (item.kind === 'textEditGroup') { - for (const group of item.edits) { - for (const edit of group) { - raw.edits.edits.push({ - resource: item.uri, - textEdit: edit, - versionId: undefined - }); - } + // replay response + const markdownContent = new MarkdownString(); + const raw: IInlineChatBulkEditResponse = { + id: Math.random(), + type: InlineChatResponseType.BulkEdit, + message: markdownContent, + edits: { edits: [] }, + }; + for (const item of response.response.value) { + if (item.kind === 'markdownContent') { + markdownContent.value += item.content.value; + } else if (item.kind === 'textEditGroup') { + for (const group of item.edits) { + for (const edit of group) { + raw.edits.edits.push({ + resource: item.uri, + textEdit: edit, + versionId: undefined + }); } } } - - inlineResponse = this._instaService.createInstance( - ReplyResponse, - raw, - markdownContent, - session.textModelN.uri, - modelAltVersionIdNow, - [], - e.request.id, - e.request.response - ); - } + + inlineResponse = this._instaService.createInstance( + ReplyResponse, + raw, + markdownContent, + session.textModelN.uri, + modelAltVersionIdNow, + [], + e.request.id, + e.request.response + ); } session.addExchange(new SessionExchange(session.lastInput!, inlineResponse)); @@ -551,38 +244,9 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { }); })); - store.add(this._chatService.onDidPerformUserAction(e => { - if (e.sessionId !== chatModel.sessionId) { - return; - } - - // TODO@jrieken VALIDATE candidate is proper, e.g check with `session.exchanges` - const request = chatModel.getRequests().find(request => request.id === e.requestId); - const candidate = request?.response?.result?.metadata?.inlineChatResponse; - - if (!candidate) { - return; - } - - let kind: InlineChatResponseFeedbackKind | undefined; - if (e.action.kind === 'vote') { - kind = e.action.direction === InteractiveSessionVoteDirection.Down ? InlineChatResponseFeedbackKind.Unhelpful : InlineChatResponseFeedbackKind.Helpful; - } else if (e.action.kind === 'bug') { - kind = InlineChatResponseFeedbackKind.Bug; - } else if (e.action.kind === 'inlineChat') { - kind = e.action.action === 'accepted' ? InlineChatResponseFeedbackKind.Accepted : InlineChatResponseFeedbackKind.Undone; - } - - if (!kind) { - return; - } - - provider.handleInlineChatResponseFeedback?.(rawSession, candidate, kind); - })); - - store.add(this._inlineChatService.onDidChangeProviders(e => { - if (e.removed === provider) { - this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${provider.extensionId}`); + store.add(this._chatAgentService.onDidChangeAgents(e => { + if (e === undefined && !this._chatAgentService.getAgent(agent.id)) { + this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${agent.extensionId}`); this._releaseSession(session, true); } })); @@ -625,7 +289,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { targetUri, textModel0, textModelN, - provider, rawSession, + agent, + rawSession, store.add(new SessionWholeRange(textModelN, wholeRange)), store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)), chatModel @@ -659,7 +324,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { found = true; this._sessions.delete(oldKey); this._sessions.set(newKey, { ...data, editor: target }); - this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.provider.extensionId}`); + this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.agent.extensionId}`); this._onDidMoveSession.fire({ session, editor: target }); break; } @@ -696,7 +361,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const [key, value] = tuple; this._sessions.delete(key); - this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.provider.extensionId}`); + this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.agent.extensionId}`); this._onDidEndSession.fire({ editor: value.editor, session, endedByExternalCause: byServer }); value.store.dispose(); @@ -706,7 +371,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { this._keepRecording(session); const result = this._instaService.createInstance(StashedSession, editor, session, undoCancelEdits); this._onDidStashSession.fire({ editor, session }); - this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.provider.extensionId}`); + this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.agent.extensionId}`); return result; } @@ -751,88 +416,24 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { } } -export class AgentInlineChatProvider implements IInlineChatSessionProvider { +export class InlineChatEnabler { - readonly extensionId: ExtensionIdentifier; - readonly label: string; - readonly supportIssueReporting?: boolean | undefined; + static Id = 'inlineChat.enabler'; + + private readonly _ctxHasProvider: IContextKey; constructor( - readonly agent: IChatAgent, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, + @IContextKeyService contextKeyService: IContextKeyService, + @IChatAgentService chatAgentService: IChatAgentService ) { - this.label = agent.name; - this.extensionId = agent.extensionId; - this.supportIssueReporting = agent.metadata.supportIssueReporting; + this._ctxHasProvider = CTX_INLINE_CHAT_HAS_PROVIDER.bindTo(contextKeyService); + chatAgentService.onDidChangeAgents(() => { + const hasEditorAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)); + this._ctxHasProvider.set(hasEditorAgent); + }); } - async prepareInlineChatSession(model: ITextModel, range: ISelection, token: CancellationToken): Promise { - - // TODO@jrieken have a good welcome message - // const welcomeMessage = await this.agent.provideWelcomeMessage?.(ChatAgentLocation.Editor, token); - // const message = welcomeMessage?.filter(candidate => typeof candidate === 'string').join(''), - - return { - id: Math.random(), - wholeRange: new Range(range.selectionStartLineNumber, range.selectionStartColumn, range.positionLineNumber, range.positionColumn), - placeholder: this.agent.description, - slashCommands: this.agent.slashCommands.map(agentCommand => { - return { - command: agentCommand.name, - detail: agentCommand.description, - refer: agentCommand.name === 'explain' // TODO@jrieken @joyceerhl this should be cleaned up - } satisfies IInlineChatSlashCommand; - }) - }; + dispose() { + this._ctxHasProvider.reset(); } - - async provideResponse(item: IInlineChatSession, request: IInlineChatRequest, progress: IProgress, token: CancellationToken): Promise { - - const workspaceEdit: WorkspaceEdit = { edits: [] }; - - await this._chatAgentService.invokeAgent(this.agent.id, { - sessionId: String(item.id), - requestId: request.requestId, - agentId: this.agent.id, - message: request.prompt, - location: ChatAgentLocation.Editor, - variables: { - variables: [{ - name: InlineChatContext.variableName, - value: JSON.stringify(new InlineChatContext(request.previewDocument, request.selection, request.wholeRange)) - }] - } - }, part => { - - if (part.kind === 'markdownContent') { - progress.report({ markdownFragment: part.content.value }); - } else if (part.kind === 'agentDetection') { - progress.report({ slashCommand: part.command?.name }); - } else if (part.kind === 'textEdit') { - - if (isEqual(request.previewDocument, part.uri)) { - progress.report({ edits: part.edits }); - } else { - for (const textEdit of part.edits) { - workspaceEdit.edits.push({ resource: part.uri, textEdit, versionId: undefined }); - } - } - } - - }, [], token); - - return { - type: InlineChatResponseType.BulkEdit, - id: Math.random(), - edits: workspaceEdit - }; - } - - // handleInlineChatResponseFeedback?(session: IInlineChatSession, response: IInlineChatResponse, kind: InlineChatResponseFeedbackKind): void { - // throw new Error('Method not implemented.'); - // } - - // provideFollowups?(session: IInlineChatSession, response: IInlineChatResponse, token: CancellationToken): ProviderResult { - // throw new Error('Method not implemented.'); - // } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 84d6a8db13e..458c0da69a6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -17,7 +17,7 @@ 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 { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; -import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, IValidEditOperation, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, IValidEditOperation, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel'; @@ -60,7 +60,6 @@ export abstract class EditModeStrategy { protected readonly _onDidAccept = this._store.add(new Emitter()); protected readonly _onDidDiscard = this._store.add(new Emitter()); - protected _editCount: number = 0; readonly onDidAccept: Event = this._onDidAccept.event; readonly onDidDiscard: Event = this._onDidDiscard.event; @@ -133,40 +132,9 @@ export abstract class EditModeStrategy { this._onDidDiscard.fire(); } - abstract makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, timings: ProgressingEditsOptions): Promise; + abstract makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, timings: ProgressingEditsOptions, undoStopBefore: boolean): Promise; - abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver): Promise; - - protected async _makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions | undefined, progress: Progress | undefined): Promise { - - // push undo stop before first edit - if (++this._editCount === 1) { - this._editor.pushUndoStop(); - } - - if (opts) { - // ASYNC - const durationInSec = opts.duration / 1000; - for (const edit of edits) { - const wordCount = countWords(edit.text ?? ''); - const speed = wordCount / durationInSec; - // console.log({ durationInSec, wordCount, speed: wordCount / durationInSec }); - const asyncEdit = asProgressiveEdit(new WindowIntervalTimer(this._zone.domNode), edit, speed, opts.token); - await performAsyncTextEdit(this._session.textModelN, asyncEdit, progress, obs); - } - - } else { - // SYNC - obs.start(); - this._session.textModelN.pushEditOperations(null, edits, (undoEdits) => { - progress?.report(undoEdits); - return null; - }); - obs.stop(); - } - } - - abstract undoChanges(altVersionId: number): Promise; + abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise; abstract renderChanges(response: ReplyResponse): Promise; @@ -216,20 +184,13 @@ export class PreviewStrategy extends EditModeStrategy { await super._doApplyChanges(false); } - override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver): Promise { + override async makeChanges(): Promise { } - override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions): Promise { + override async makeProgressiveChanges(): Promise { } - override async undoChanges(altVersionId: number): Promise { - const { textModelN } = this._session; - await undoModelUntil(textModelN, altVersionId); - } - - override async renderChanges(response: ReplyResponse): Promise { - - } + override async renderChanges(response: ReplyResponse): Promise { } hasFocus(): boolean { return this._zone.widget.hasFocus(); @@ -289,6 +250,7 @@ export class LiveStrategy extends EditModeStrategy { private readonly _ctxCurrentChangeShowsDiff: IContextKey; private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; + private _editCount: number = 0; override acceptHunk: () => Promise = () => super.acceptHunk(); override discardHunk: () => Promise = () => super.discardHunk(); @@ -342,16 +304,11 @@ export class LiveStrategy extends EditModeStrategy { return super.cancel(); } - override async undoChanges(altVersionId: number): Promise { - const { textModelN } = this._session; - await undoModelUntil(textModelN, altVersionId); + override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise { + return this._makeChanges(edits, obs, undefined, undefined, undoStopBefore); } - override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver): Promise { - return this._makeChanges(edits, obs, undefined, undefined); - } - - override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions): Promise { + override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean): Promise { // add decorations once per line that got edited const progress = new Progress(edits => { @@ -371,7 +328,38 @@ export class LiveStrategy extends EditModeStrategy { this._progressiveEditingDecorations.append(newDecorations); }); - return this._makeChanges(edits, obs, opts, progress); + return this._makeChanges(edits, obs, opts, progress, undoStopBefore); + } + + private async _makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions | undefined, progress: Progress | undefined, undoStopBefore: boolean): Promise { + + // push undo stop before first edit + if (undoStopBefore) { + this._editor.pushUndoStop(); + } + + this._editCount++; + + if (opts) { + // ASYNC + const durationInSec = opts.duration / 1000; + for (const edit of edits) { + const wordCount = countWords(edit.text ?? ''); + const speed = wordCount / durationInSec; + // console.log({ durationInSec, wordCount, speed: wordCount / durationInSec }); + const asyncEdit = asProgressiveEdit(new WindowIntervalTimer(this._zone.domNode), edit, speed, opts.token); + await performAsyncTextEdit(this._session.textModelN, asyncEdit, progress, obs); + } + + } else { + // SYNC + obs.start(); + this._session.textModelN.pushEditOperations(null, edits, (undoEdits) => { + progress?.report(undoEdits); + return null; + }); + obs.stop(); + } } private readonly _hunkDisplayData = new Map(); @@ -590,17 +578,17 @@ 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 1 change") : localize('change.1', "1 change"); } else { message = needsReview - ? localize('review.N', "$(info) Accept or Discard {0} changes.", remaining) + ? localize('review.N', "$(info) Accept or Discard {0} changes", remaining) : localize('change.N', "{0} changes", total); } let title: string | undefined; if (needsReview) { - title = localize('review', "Review (accept or discard) all changes before continuing."); + title = localize('review', "Review (accept or discard) all changes before continuing"); } this._zone.widget.updateStatus(message, { title }); @@ -616,14 +604,6 @@ export class LiveStrategy extends EditModeStrategy { } } - -async function undoModelUntil(model: ITextModel, targetAltVersion: number): Promise { - while (targetAltVersion < model.getAlternativeVersionId() && model.canUndo()) { - await model.undo(); - } -} - - function changeDecorationsAndViewZones(editor: ICodeEditor, callback: (accessor: IModelDecorationsChangeAccessor, viewZoneAccessor: IViewZoneChangeAccessor) => void): void { editor.changeDecorations(decorationsAccessor => { editor.changeViewZones(viewZoneAccessor => { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 5a122b0fe0a..efe64a7d919 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -32,13 +32,12 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { asCssVariable, asCssVariableName, editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; -import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatModel, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { isRequestVM, 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, IInlineChatFollowup, IInlineChatSlashCommand, inlineChatBackground } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +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 { chatRequestBackground } from 'vs/workbench/contrib/chat/common/chatColors'; import { Selection } from 'vs/editor/common/core/selection'; @@ -131,7 +130,8 @@ export class InlineChatWidget { private _isLayouting: boolean = false; - private readonly _followUpDisposables = this._store.add(new DisposableStore()); + readonly scopedContextKeyService: IContextKeyService; + constructor( location: ChatAgentLocation, options: IInlineChatWidgetConstructionOptions, @@ -151,12 +151,13 @@ export class InlineChatWidget { let allowRequests = false; - + this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)); const scopedInstaService = _instantiationService.createChild( new ServiceCollection([ IContextKeyService, - this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)) - ]) + this.scopedContextKeyService + ]), + this._store ); this._chatWidget = scopedInstaService.createInstance( @@ -380,17 +381,17 @@ export class InlineChatWidget { // The chat widget is variable height and supports scrolling. It should be // at least "maxWidgetHeight" high and at most the content height. - let maxWidgetHeight = 100; + let maxWidgetOutputHeight = 100; for (const item of this._chatWidget.viewModel?.getItems() ?? []) { if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) { - maxWidgetHeight = 270; + maxWidgetOutputHeight = 270; break; } } let value = this.contentHeight; value -= this._chatWidget.contentHeight; - value += Math.min(maxWidgetHeight, this._chatWidget.contentHeight); + value += Math.min(this._chatWidget.input.contentHeight + maxWidgetOutputHeight, this._chatWidget.contentHeight); return value; } @@ -527,28 +528,6 @@ export class InlineChatWidget { } }; } - /** - * @deprecated use `setChatModel` instead - */ - updateFollowUps(items: IInlineChatFollowup[], onFollowup: (followup: IInlineChatFollowup) => void): void; - updateFollowUps(items: undefined): void; - updateFollowUps(items: IInlineChatFollowup[] | undefined, onFollowup?: ((followup: IInlineChatFollowup) => void)) { - this._followUpDisposables.clear(); - this._elements.followUps.classList.toggle('hidden', !items || items.length === 0); - reset(this._elements.followUps); - if (items && items.length > 0 && onFollowup) { - this._followUpDisposables.add( - this._instantiationService.createInstance(ChatFollowups, this._elements.followUps, items, ChatAgentLocation.Editor, undefined, onFollowup)); - } - this._onDidChangeHeight.fire(); - } - - /** - * @deprecated use `setChatModel` instead - */ - updateSlashCommands(commands: IInlineChatSlashCommand[]) { - - } updateInfo(message: string): void { this._elements.infoLabel.classList.toggle('hidden', !message); @@ -588,7 +567,6 @@ export class InlineChatWidget { reset() { this._chatWidget.saveState(); this.updateChatMessage(undefined); - this.updateFollowUps(undefined); reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index bd3840d06c4..5dcca5cbb31 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.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 { Dimension } from 'vs/base/browser/dom'; +import { addDisposableListener, Dimension } from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { toDisposable } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; @@ -14,7 +14,7 @@ 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, ACTION_VIEW_IN_CHAT, 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_WIDGET, 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'; @@ -53,9 +53,9 @@ export class InlineChatZoneWidget extends ZoneWidget { menu: MENU_INLINE_CHAT_WIDGET_STATUS, options: { buttonConfigProvider: action => { - if (action.id === ACTION_REGENERATE_RESPONSE || action.id === ACTION_TOGGLE_DIFF) { - return { showIcon: true, showLabel: false, isSecondary: true }; - } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { + if (new Set([ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF]).has(action.id)) { + return { isSecondary: true, showIcon: true, showLabel: false }; + } else if (action.id === ACTION_ACCEPT_CHANGES) { return { isSecondary: false }; } else { return { isSecondary: true }; @@ -81,6 +81,13 @@ export class InlineChatZoneWidget extends ZoneWidget { this._disposables.add(this.widget); this.create(); + this._disposables.add(addDisposableListener(this.domNode, 'click', e => { + if (!this.editor.hasWidgetFocus() && !this.widget.hasFocus()) { + this.editor.focus(); + } + }, true)); + + // todo@jrieken listen ONLY when showing const updateCursorIsAboveContextKey = () => { if (!this.position || !this.editor.hasModel()) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 28812fb55d1..e7b6f8a6c0b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -17,11 +17,11 @@ .monaco-workbench .inline-chat { color: inherit; - padding: 6px; - border-radius: 6px; + 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; - box-shadow: 0 -1px 6px var(--vscode-inlineChat-shadow); background: var(--vscode-inlineChat-background); } @@ -31,10 +31,24 @@ .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-input-part .interactive-input-and-execute-toolbar { width: 100%; + border-radius: 2px; +} + +.monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list { + padding: 4px 0 0 0; } .monaco-workbench .inline-chat .chat-widget .interactive-session .interactive-list .interactive-item-container.interactive-item-compact { - padding: 4px + padding: 6px 4px; + gap: 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 { + border: none; } /* progress bit */ @@ -56,7 +70,6 @@ align-items: center; } - .monaco-workbench .inline-chat .status .actions.hidden { display: none; } @@ -64,9 +77,7 @@ .monaco-workbench .inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); - font-size: 11px; - padding-top: 4px; - padding-left: 10px; + font-size: 12px; display: inline-flex; } @@ -131,7 +142,6 @@ .monaco-workbench .inline-chat .status .actions { display: flex; - padding-top: 4px; } .monaco-workbench .inline-chat .status .actions > .monaco-button, @@ -239,7 +249,7 @@ .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: 4px; + border-radius: 2px; padding: 1px; } diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index fc2da4a247d..241986e3f3b 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -3,53 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { IRange } from 'vs/editor/common/core/range'; -import { ISelection } from 'vs/editor/common/core/selection'; -import { ProviderResult, TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; -import { ITextModel } from 'vs/editor/common/model'; +import { TextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IProgress } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { diffInserted, diffRemoved, editorHoverHighlight, editorWidgetBackground, editorWidgetBorder, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { Extensions as ExtensionsMigration, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; -import { URI } from 'vs/base/common/uri'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; - -export interface IInlineChatSlashCommand { - command: string; - detail?: string; - refer?: boolean; - executeImmediately?: boolean; -} +import { IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; export interface IInlineChatSession { id: number; placeholder?: string; input?: string; message?: string; - slashCommands?: IInlineChatSlashCommand[]; + slashCommands?: IChatAgentCommand[]; wholeRange?: IRange; } -export interface IInlineChatRequest { - prompt: string; - selection: ISelection; - wholeRange: IRange; - attempt: number; - requestId: string; - live: boolean; - previewDocument: URI; - withIntentDetection: boolean; -} - export type IInlineChatResponse = IInlineChatEditResponse | IInlineChatBulkEditResponse; export const enum InlineChatResponseType { @@ -82,72 +56,15 @@ export interface IInlineChatBulkEditResponse { wholeRange?: IRange; } -export interface IInlineChatProgressItem { - markdownFragment?: string; - edits?: TextEdit[]; - editsShouldBeInstant?: boolean; - message?: string; - slashCommand?: string; -} - -export const enum InlineChatResponseFeedbackKind { - Unhelpful = 0, - Helpful = 1, - Undone = 2, - Accepted = 3, - Bug = 4 -} - -export interface IInlineChatReplyFollowup { - kind: 'reply'; - message: string; - title?: string; - tooltip?: string; -} - -export interface IInlineChatCommandFollowup { - kind: 'command'; - commandId: string; - args?: any[]; - title: string; // supports codicon strings - when?: string; -} - -export type IInlineChatFollowup = IInlineChatReplyFollowup | IInlineChatCommandFollowup; - -export interface IInlineChatSessionProvider { - - extensionId: ExtensionIdentifier; - label: string; - supportIssueReporting?: boolean; - - prepareInlineChatSession(model: ITextModel, range: ISelection, token: CancellationToken): ProviderResult; - - provideResponse(item: IInlineChatSession, request: IInlineChatRequest, progress: IProgress, token: CancellationToken): ProviderResult; - - provideFollowups?(session: IInlineChatSession, response: IInlineChatResponse, token: CancellationToken): ProviderResult; - - handleInlineChatResponseFeedback?(session: IInlineChatSession, response: IInlineChatResponse, kind: InlineChatResponseFeedbackKind): void; -} - -export const IInlineChatService = createDecorator('IInlineChatService'); - -export interface InlineChatProviderChangeEvent { - readonly added?: IInlineChatSessionProvider; - readonly removed?: IInlineChatSessionProvider; -} - -export interface IInlineChatService { - _serviceBrand: undefined; - - onDidChangeProviders: Event; - addProvider(provider: IInlineChatSessionProvider): IDisposable; - getAllProvider(): Iterable; -} export const INLINE_CHAT_ID = 'interactiveEditor'; export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccessiblityHelp'; +export const enum EditMode { + Live = 'live', + Preview = 'preview' +} + export const CTX_INLINE_CHAT_HAS_PROVIDER = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible")); export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused")); @@ -182,7 +99,6 @@ export const ACTION_TOGGLE_DIFF = 'inlineChat.toggleDiff'; export const MENU_INLINE_CHAT_WIDGET = MenuId.for('inlineChatWidget'); export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); -export const MENU_INLINE_CHAT_WIDGET_DISCARD = MenuId.for('inlineChatWidget.undo'); // --- colors @@ -206,10 +122,7 @@ export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewR // settings -export const enum EditMode { - Live = 'live', - Preview = 'preview' -} + Registry.as(ExtensionsMigration.ConfigurationMigration).registerConfigurationMigrations( [{ diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl.ts deleted file mode 100644 index ddff44a1b7f..00000000000 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl.ts +++ /dev/null @@ -1,42 +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, toDisposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; -import { LinkedList } from 'vs/base/common/linkedList'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInlineChatService, IInlineChatSessionProvider, CTX_INLINE_CHAT_HAS_PROVIDER, InlineChatProviderChangeEvent } from './inlineChat'; - -export class InlineChatServiceImpl implements IInlineChatService { - - declare _serviceBrand: undefined; - - private readonly _onDidChangeProviders = new Emitter(); - private readonly _entries = new LinkedList(); - private readonly _ctxHasProvider: IContextKey; - - readonly onDidChangeProviders = this._onDidChangeProviders.event; - - constructor(@IContextKeyService contextKeyService: IContextKeyService) { - this._ctxHasProvider = CTX_INLINE_CHAT_HAS_PROVIDER.bindTo(contextKeyService); - } - - addProvider(provider: IInlineChatSessionProvider): IDisposable { - - const rm = this._entries.push(provider); - this._ctxHasProvider.set(true); - this._onDidChangeProviders.fire({ added: provider }); - - return toDisposable(() => { - rm(); - this._ctxHasProvider.set(this._entries.size > 0); - this._onDidChangeProviders.fire({ removed: provider }); - }); - } - - getAllProvider() { - return [...this._entries].reverse(); - } -} 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 e202d8dfe58..497e80085f2 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -29,14 +29,13 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatRequest, IInlineChatService, InlineChatConfigKeys, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; +import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, InlineChatConfigKeys } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInlineChatSavingService } from '../../browser/inlineChatSavingService'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService'; @@ -62,6 +61,20 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; suite('InteractiveChatController', function () { + + const agentData = { + extensionId: nullExtensionDescription.identifier, + publisherDisplayName: '', + extensionDisplayName: '', + extensionPublisherId: '', + // id: 'testEditorAgent', + name: 'testEditorAgent', + isDefault: true, + locations: [ChatAgentLocation.Editor], + metadata: {}, + slashCommands: [] + }; + class TestController extends InlineChatController { static INIT_SEQUENCE: readonly State[] = [State.CREATE_SESSION, State.INIT_UI, State.WAIT_FOR_INPUT]; @@ -112,7 +125,7 @@ suite('InteractiveChatController', function () { let model: ITextModel; let ctrl: TestController; let contextKeyService: MockContextKeyService; - let inlineChatService: InlineChatServiceImpl; + let chatAgentService: IChatAgentService; let inlineChatSessionService: IInlineChatSessionService; let instaService: TestInstantiationService; @@ -135,7 +148,6 @@ suite('InteractiveChatController', function () { [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], [IChatAgentService, new SyncDescriptor(ChatAgentService)], - [IInlineChatService, new SyncDescriptor(InlineChatServiceImpl)], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], [ICommandService, new SyncDescriptor(TestCommandService)], @@ -177,50 +189,27 @@ suite('InteractiveChatController', function () { contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService; - inlineChatService = instaService.get(IInlineChatService) as InlineChatServiceImpl; + chatAgentService = instaService.get(IChatAgentService); - const chatAgentService = instaService.get(IChatAgentService); - - store.add(chatAgentService.registerDynamicAgent({ - extensionId: nullExtensionDescription.identifier, - publisherDisplayName: '', - extensionDisplayName: '', - extensionPublisherId: '', - id: 'testAgent', - name: 'testAgent', - isDefault: true, - locations: [ChatAgentLocation.Panel], - metadata: {}, - slashCommands: [] - }, { - async invoke(request, progress, history, token) { - return {}; - }, - })); inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); editor = store.add(instantiateTestCodeEditor(instaService, model)); - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test Default', - prepareInlineChatSession() { - return { - id: Math.random() - }; - }, - provideResponse(session, request) { - return { - type: InlineChatResponseType.EditorEdit, - id: Math.random(), + store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, { + async invoke(request, progress, history, token) { + progress({ + kind: 'textEdit', + uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), - text: request.prompt + text: request.message }] - }; - } + }); + return {}; + }, })); + }); teardown(function () { @@ -255,48 +244,6 @@ suite('InteractiveChatController', function () { editor.setSelection(new Range(1, 1, 1, 3)); ctrl = instaService.createInstance(TestController, editor); - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random() - }; - }, - provideResponse(session, request) { - throw new Error(); - } - })); - - ctrl.run({}); - await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT)); - - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assert.ok(session); - assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 3)); - - await ctrl.cancelSession(); - }); - - test('wholeRange expands to whole lines, session provided', async function () { - - editor.setSelection(new Range(1, 1, 1, 1)); - ctrl = instaService.createInstance(TestController, editor); - - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - wholeRange: new Range(1, 1, 1, 3) - }; - }, - provideResponse(session, request) { - throw new Error(); - } - })); - ctrl.run({}); await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT)); @@ -330,27 +277,24 @@ suite('InteractiveChatController', function () { test('\'whole range\' isn\'t updated for edits outside whole range #4346', async function () { - editor.setSelection(new Range(3, 1, 3, 1)); + editor.setSelection(new Range(3, 1, 3, 3)); - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - wholeRange: new Range(3, 1, 3, 3) - }; - }, - provideResponse(session, request) { - return { - type: InlineChatResponseType.EditorEdit, - id: Math.random(), + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + progress({ + kind: 'textEdit', + uri: editor.getModel().uri, edits: [{ range: new Range(1, 1, 1, 1), // EDIT happens outside of whole range - text: `${request.prompt}\n${request.prompt}` + text: `${request.message}\n${request.message}` }] - }; - } + }); + + return {}; + }, })); ctrl = instaService.createInstance(TestController, editor); @@ -373,19 +317,16 @@ suite('InteractiveChatController', function () { }); test('Stuck inline chat widget #211', async function () { - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - wholeRange: new Range(3, 1, 3, 3) - }; - }, - provideResponse(session, request) { + + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { return new Promise(() => { }); - } + }, })); + ctrl = instaService.createInstance(TestController, editor); const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); const r = ctrl.run({ message: 'Hello', autoSend: true }); @@ -399,26 +340,18 @@ suite('InteractiveChatController', function () { test('[Bug] Inline Chat\'s streaming pushed broken iterations to the undo stack #2403', async function () { - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - wholeRange: new Range(3, 1, 3, 3) - }; + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { + + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'hEllo1\n' }] }); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(2, 1, 2, 1), text: 'hEllo2\n' }] }); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1000, 1), text: 'Hello1\nHello2\n' }] }); + + return {}; }, - async provideResponse(session, request, progress) { - - progress.report({ edits: [{ range: new Range(1, 1, 1, 1), text: 'hEllo1\n' }] }); - progress.report({ edits: [{ range: new Range(2, 1, 2, 1), text: 'hEllo2\n' }] }); - - return { - id: Math.random(), - type: InlineChatResponseType.EditorEdit, - edits: [{ range: new Range(1, 1, 1000, 1), text: 'Hello1\nHello2\n' }] - }; - } })); const valueThen = editor.getModel().getValue(); @@ -444,26 +377,22 @@ suite('InteractiveChatController', function () { return runWithFakedTimers({ maxTaskCount: Number.MAX_SAFE_INTEGER }, async () => { - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random(), - }; - }, - async provideResponse(session, request, progress) { + store.add(chatAgentService.registerDynamicAgent({ + id: 'testEditorAgent2', + ...agentData + }, { + async invoke(request, progress, history, token) { const text = '${CSI}#a\n${CSI}#b\n${CSI}#c\n'; await timeout(10); - progress.report({ edits: [{ range: new Range(1, 1, 1, 1), text: text }] }); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text }] }); await timeout(10); - progress.report({ edits: [{ range: new Range(1, 1, 1, 1), text: text.repeat(1000) + 'DONE' }] }); + progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.repeat(1000) + 'DONE' }] }); throw new Error('Too long'); - } + }, })); @@ -525,46 +454,4 @@ suite('InteractiveChatController', function () { assert.ok(model.getValue().includes('MANUAL')); }); - - test('context has correct preview document', async function () { - - const requests: IInlineChatRequest[] = []; - - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random() - }; - }, - provideResponse(_session, request) { - requests.push(request); - return undefined; - } - })); - - async function makeRequest() { - const p = ctrl.waitFor(TestController.INIT_SEQUENCE_AUTO_SEND); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - await p; - await ctrl.cancelSession(); - await r; - } - - // manual edits -> finish - ctrl = instaService.createInstance(TestController, editor); - - configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Live }); - await makeRequest(); - - - configurationService.setUserConfiguration('inlineChat', { mode: EditMode.Preview }); - await makeRequest(); - - assert.strictEqual(requests.length, 2); - - assert.strictEqual(requests[0].previewDocument.toString(), model.uri.toString()); // live - assert.strictEqual(requests[1].previewDocument.toString(), model.uri.toString()); // preview (both use the same but edits aren't applied like that) - }); }); 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 4e3f576d720..3731977e58b 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -24,18 +24,16 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; import { HunkState, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { InlineChatSessionServiceImpl } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl'; -import { EditMode, IInlineChatService, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { EditMode } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { CancellationToken } from 'vs/base/common/cancellation'; import { assertType } from 'vs/base/common/types'; -import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; @@ -57,6 +55,7 @@ import { IChatAgentService, ChatAgentService, ChatAgentLocation } from 'vs/workb import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { TestCommandService } from 'vs/editor/test/browser/editorTestServices'; +import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; suite('InlineChatSession', function () { @@ -64,7 +63,6 @@ suite('InlineChatSession', function () { let editor: IActiveCodeEditor; let model: ITextModel; let instaService: TestInstantiationService; - let inlineChatService: InlineChatServiceImpl; let inlineChatSessionService: IInlineChatSessionService; @@ -87,7 +85,6 @@ suite('InlineChatSession', function () { [IChatService, new SyncDescriptor(ChatService)], [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IChatAgentService, new SyncDescriptor(ChatAgentService)], - [IInlineChatService, new SyncDescriptor(InlineChatServiceImpl)], [IContextKeyService, contextKeyService], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], @@ -124,8 +121,6 @@ suite('InlineChatSession', function () { instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); - - inlineChatService = instaService.get(IInlineChatService) as InlineChatServiceImpl; inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); instaService.get(IChatAgentService).registerDynamicAgent({ @@ -136,7 +131,7 @@ suite('InlineChatSession', function () { id: 'testAgent', name: 'testAgent', isDefault: true, - locations: [ChatAgentLocation.Panel], + locations: [ChatAgentLocation.Editor], metadata: {}, slashCommands: [] }, { @@ -145,25 +140,7 @@ suite('InlineChatSession', function () { } }); - store.add(inlineChatService.addProvider({ - extensionId: nullExtensionDescription.identifier, - label: 'Unit Test', - prepareInlineChatSession() { - return { - id: Math.random() - }; - }, - provideResponse(session, request) { - return { - type: InlineChatResponseType.EditorEdit, - id: Math.random(), - edits: [{ - range: new Range(1, 1, 1, 1), - text: request.prompt - }] - }; - } - })); + model = store.add(instaService.get(IModelService).createModel('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven', null)); editor = store.add(instantiateTestCodeEditor(instaService, model)); }); diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 3a6737053eb..cc21b0371a6 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -434,7 +434,21 @@ registerAction2(class extends Action2 { id: 'interactive.execute', title: localize2('interactive.execute', 'Execute Code'), category: interactiveWindowCategory, - keybinding: { + keybinding: [{ + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), + ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', true) + ), + primary: KeyMod.Shift | KeyCode.Enter, + weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT + }, { + when: ContextKeyExpr.and( + ContextKeyExpr.equals('activeEditor', 'workbench.editor.interactive'), + ContextKeyExpr.equals('config.interactiveWindow.executeWithShiftEnter', false) + ), + 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, @@ -442,7 +456,7 @@ registerAction2(class extends Action2 { primary: KeyMod.CtrlCmd | KeyCode.Enter }, weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT - }, + }], menu: [ { id: MenuId.InteractiveInputExecute @@ -794,19 +808,16 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'boolean', default: true, markdownDescription: localize('interactiveWindow.alwaysScrollOnNewCell', "Automatically scroll the interactive window to show the output of the last statement executed. If this value is false, the window will only scroll if the last cell was already the one scrolled to.") - } - } -}); - -Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ - id: 'interactiveWindow', - order: 100, - type: 'object', - 'properties': { + }, [NotebookSetting.InteractiveWindowPromptToSave]: { type: 'boolean', 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']: { + 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.") } } }); diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index f9fa228eb39..fd3f51d3560 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -72,9 +72,9 @@ class LanguageStatusContribution extends Disposable implements IWorkbenchContrib super(); // --- main language status - const mainInstantiationService = instantiationService.createChild(new ServiceCollection( + const mainInstantiationService = this._register(instantiationService.createChild(new ServiceCollection( [IEditorService, editorService.createScoped('main', this._store)] - )); + ))); this._register(mainInstantiationService.createInstance(LanguageStatus)); // --- auxiliary language status diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts index acd3ca80d4a..8d30e92f19e 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts @@ -151,7 +151,7 @@ export class LocalHistoryTimeline extends Disposable implements IWorkbenchContri return { handle: entry.id, label: SaveSourceRegistry.getSourceLabel(entry.source), - tooltip: new MarkdownString(`$(history) ${getLocalHistoryDateFormatter().format(entry.timestamp)}\n\n${SaveSourceRegistry.getSourceLabel(entry.source)}`, { supportThemeIcons: true }), + tooltip: new MarkdownString(`$(history) ${getLocalHistoryDateFormatter().format(entry.timestamp)}\n\n${SaveSourceRegistry.getSourceLabel(entry.source)}${entry.sourceDescription ? ` (${entry.sourceDescription})` : ``}`, { supportThemeIcons: true }), source: this.id, timestamp: entry.timestamp, themeIcon: LOCAL_HISTORY_ICON_ENTRY, diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 3ececbb1223..86c59a48995 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -171,7 +171,7 @@ export class OpenWindowSessionLogFileAction extends Action { override async run(): Promise { const sessionResult = await this.quickInputService.pick( - this.getSessions().then(sessions => sessions.map((s, index) => ({ + this.getSessions().then(sessions => sessions.map((s, index): IQuickPickItem => ({ id: s.toString(), label: basename(s), description: index === 0 ? nls.localize('current', "Current") : undefined @@ -182,7 +182,7 @@ export class OpenWindowSessionLogFileAction extends Action { }); if (sessionResult) { const logFileResult = await this.quickInputService.pick( - this.getLogFiles(URI.parse(sessionResult.id!)).then(logFiles => logFiles.map(s => ({ + this.getLogFiles(URI.parse(sessionResult.id!)).then(logFiles => logFiles.map((s): IQuickPickItem => ({ id: s.toString(), label: basename(s) }))), diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 169dc418962..2e7f61cc621 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -92,7 +92,7 @@ export class MarkersFilters extends Disposable { set showErrors(showErrors: boolean) { if (this._showErrors.get() !== showErrors) { this._showErrors.set(showErrors); - this._onDidChange.fire({ showErrors: true }); + this._onDidChange.fire({ showErrors: true }); } } @@ -103,7 +103,7 @@ export class MarkersFilters extends Disposable { set showInfos(showInfos: boolean) { if (this._showInfos.get() !== showInfos) { this._showInfos.set(showInfos); - this._onDidChange.fire({ showInfos: true }); + this._onDidChange.fire({ showInfos: true }); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts index 39659a12926..5b6205106fe 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts @@ -487,7 +487,7 @@ async function mergeEditorCompare(viewModel: MergeEditorViewModel, editorService }, revealIfOpened: true, revealIfVisible: true, - } as ITextEditorOptions + } satisfies ITextEditorOptions }); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts index 278615a60b2..56760afd5eb 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts @@ -11,7 +11,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; -import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { LineRange as DiffLineRange } from 'vs/editor/common/core/lineRange'; export interface IMergeDiffComputer { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index c085272472c..abc62bf6b7e 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -6,10 +6,9 @@ import { ArrayQueue, CompareResult } from 'vs/base/common/arrays'; import { onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorunOpts, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorunOpts } from 'vs/base/common/observable'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export function setStyle( @@ -104,7 +103,7 @@ export function setFields(obj: T, fields: Partial): T { } export function deepMerge(source1: T, source2: Partial): T { - const result = {} as T; + const result = {} as any as T; for (const key in source1) { result[key] = source1[key]; } @@ -156,13 +155,3 @@ export class PersistentStore { } } -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} 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 8cd243e3777..29af08fbafe 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -20,7 +20,8 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; -import { observableConfigValue, setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; export abstract class CodeEditorView extends Disposable { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 3609b0046fa..bec1dec9481 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -39,7 +39,8 @@ import { readTransientState, writeTransientState } from 'vs/workbench/contrib/co import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { IMergeEditorInputModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; -import { deepMerge, observableConfigValue, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { deepMerge, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { ScrollSynchronizer } from 'vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index ca8a00e6b56..1c0f093dc27 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -15,7 +15,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; import { InputNumber, ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; -import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { CodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView'; import { InputCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts index a04c894f931..79608fb88b4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts @@ -7,7 +7,6 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; import { IRange } from 'vs/editor/common/core/range'; import { ICellExecutionError, ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -16,6 +15,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; type CellDiagnostic = { cellUri: URI; @@ -35,14 +35,14 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri private readonly notebookEditor: INotebookEditor, @INotebookExecutionStateService private readonly notebookExecutionStateService: INotebookExecutionStateService, @IMarkerService private readonly markerService: IMarkerService, - @IInlineChatService private readonly inlineChatService: IInlineChatService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.updateEnabled(); - this._register(inlineChatService.onDidChangeProviders(() => this.updateEnabled())); + this._register(chatAgentService.onDidChangeAgents(() => this.updateEnabled())); this._register(configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration(NotebookSetting.cellFailureDiagnostics)) { this.updateEnabled(); @@ -52,10 +52,10 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri private updateEnabled() { const settingEnabled = this.configurationService.getValue(NotebookSetting.cellFailureDiagnostics); - if (this.enabled && (!settingEnabled || Iterable.isEmpty(this.inlineChatService.getAllProvider()))) { + if (this.enabled && (!settingEnabled || Iterable.isEmpty(this.chatAgentService.getAgents()))) { this.enabled = false; this.clearAll(); - } else if (!this.enabled && settingEnabled && !Iterable.isEmpty(this.inlineChatService.getAllProvider())) { + } else if (!this.enabled && settingEnabled && !Iterable.isEmpty(this.chatAgentService.getAgents())) { this.enabled = true; if (!this.listening) { this.listening = true; 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 2ee5da0b02b..eeef10e64d2 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts @@ -12,9 +12,9 @@ import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { EmptyTextEditorHintContribution, IEmptyTextEditorHintOptions } from 'vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint'; import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -30,7 +30,7 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu @IHoverService hoverService: IHoverService, @IKeybindingService keybindingService: IKeybindingService, @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, - @IInlineChatService inlineChatService: IInlineChatService, + @IChatAgentService chatAgentService: IChatAgentService, @ITelemetryService telemetryService: ITelemetryService, @IProductService productService: IProductService ) { @@ -42,7 +42,7 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu hoverService, keybindingService, inlineChatSessionService, - inlineChatService, + chatAgentService, telemetryService, productService ); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index 7cb9a68437a..166de405964 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -14,7 +14,7 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { FormattingMode, formatDocumentWithSelectedProvider, getDocumentFormattingEditsUntilResult } from 'vs/editor/contrib/format/browser/format'; +import { FormattingMode, formatDocumentWithSelectedProvider, getDocumentFormattingEditsWithSelectedProvider } from 'vs/editor/contrib/format/browser/format'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -78,11 +78,11 @@ registerAction2(class extends Action2 { const model = ref.object.textEditorModel; - const formatEdits = await getDocumentFormattingEditsUntilResult( + const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( editorWorkerService, languageFeaturesService, model, - model.getOptions(), + FormattingMode.Explicit, CancellationToken.None ); @@ -177,12 +177,11 @@ class FormatOnCellExecutionParticipant implements ICellExecutionParticipant { const model = ref.object.textEditorModel; - // todo: eventually support cancellation. potential leak if cell deleted mid execution - const formatEdits = await getDocumentFormattingEditsUntilResult( + const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( this.editorWorkerService, this.languageFeaturesService, model, - model.getOptions(), + FormattingMode.Silent, CancellationToken.None ); 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 31c62508ca9..89b90bbff4b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts @@ -19,12 +19,12 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ApplyCodeActionReason, applyCodeAction, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; -import { getDocumentFormattingEditsUntilResult } from 'vs/editor/contrib/format/browser/format'; +import { CodeActionItem, CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; +import { FormattingMode, getDocumentFormattingEditsWithSelectedProvider } from 'vs/editor/contrib/format/browser/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -32,6 +32,7 @@ import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/w import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -43,6 +44,7 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { constructor( @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ITextModelService private readonly textModelService: ITextModelService, @IBulkEditService private readonly bulkEditService: IBulkEditService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -61,38 +63,40 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { if (!enabled) { return undefined; } + progress.report({ message: localize('notebookFormatSave.formatting', "Formatting") }); const notebook = workingCopy.model.notebookModel; + const formatApplied: boolean = await this.instantiationService.invokeFunction(CodeActionParticipantUtils.checkAndRunFormatCodeAction, notebook, progress, token); - progress.report({ message: localize('notebookFormatSave.formatting', "Formatting") }); const disposable = new DisposableStore(); try { - const allCellEdits = await Promise.all(notebook.cells.map(async cell => { - const ref = await this.textModelService.createModelReference(cell.uri); - disposable.add(ref); + if (!formatApplied) { + const allCellEdits = await Promise.all(notebook.cells.map(async cell => { + const ref = await this.textModelService.createModelReference(cell.uri); + disposable.add(ref); - const model = ref.object.textEditorModel; + const model = ref.object.textEditorModel; - const formatEdits = await getDocumentFormattingEditsUntilResult( - this.editorWorkerService, - this.languageFeaturesService, - model, - model.getOptions(), - token - ); + const formatEdits = await getDocumentFormattingEditsWithSelectedProvider( + this.editorWorkerService, + this.languageFeaturesService, + model, + FormattingMode.Silent, + token + ); - const edits: ResourceTextEdit[] = []; + const edits: ResourceTextEdit[] = []; - if (formatEdits) { - edits.push(...formatEdits.map(edit => new ResourceTextEdit(model.uri, edit, model.getVersionId()))); - return edits; - } + if (formatEdits) { + edits.push(...formatEdits.map(edit => new ResourceTextEdit(model.uri, edit, model.getVersionId()))); + return edits; + } - return []; - })); - - await this.bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('formatNotebook', "Format Notebook"), code: 'undoredo.formatNotebook', }); + return []; + })); + await this.bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('formatNotebook', "Format Notebook"), code: 'undoredo.formatNotebook', }); + } } finally { progress.report({ increment: 100 }); disposable.dispose(); @@ -317,14 +321,12 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa @IConfigurationService private readonly configurationService: IConfigurationService, @ILogService private readonly logService: ILogService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @ITextModelService private readonly textModelService: ITextModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { } async participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { - const nbDisposable = new DisposableStore(); const isTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted(); if (!isTrusted) { return; @@ -350,15 +352,9 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa const notebookModel = workingCopy.model.notebookModel; const setting = this.configurationService.getValue<{ [kind: string]: string | boolean }>(NotebookSetting.codeActionsOnSave); - if (!setting) { - return undefined; - } const settingItems: string[] = Array.isArray(setting) ? setting : Object.keys(setting).filter(x => setting[x]); - if (!settingItems.length) { - return undefined; - } const allCodeActions = this.createCodeActionsOnSave(settingItems); const excludedActions = allCodeActions @@ -368,60 +364,62 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa const editorCodeActionsOnSave = includedActions.filter(x => !CodeActionKind.Notebook.contains(x)); const notebookCodeActionsOnSave = includedActions.filter(x => CodeActionKind.Notebook.contains(x)); - if (!editorCodeActionsOnSave.length && !notebookCodeActionsOnSave.length) { - return undefined; - } - - // prioritize `source.fixAll` code actions - if (!Array.isArray(setting)) { - editorCodeActionsOnSave.sort((a, b) => { - if (CodeActionKind.SourceFixAll.contains(a)) { - if (CodeActionKind.SourceFixAll.contains(b)) { - return 0; - } - return -1; - } - if (CodeActionKind.SourceFixAll.contains(b)) { - return 1; - } - return 0; - }); - } // run notebook code actions - progress.report({ message: localize('notebookSaveParticipants.notebookCodeActions', "Running 'Notebook' code actions") }); - try { - const cell = notebookModel.cells[0]; - const ref = await this.textModelService.createModelReference(cell.uri); - nbDisposable.add(ref); - - const textEditorModel = ref.object.textEditorModel; - - await this.applyOnSaveActions(textEditorModel, notebookCodeActionsOnSave, excludedActions, progress, token); - } catch { - this.logService.error('Failed to apply notebook code action on save'); - } finally { - progress.report({ increment: 100 }); - nbDisposable.dispose(); - } - - // run cell level code actions - const disposable = new DisposableStore(); - progress.report({ message: localize('notebookSaveParticipants.cellCodeActions', "Running 'Cell' code actions") }); - try { - await Promise.all(notebookModel.cells.map(async cell => { + if (notebookCodeActionsOnSave.length) { + const nbDisposable = new DisposableStore(); + progress.report({ message: localize('notebookSaveParticipants.notebookCodeActions', "Running 'Notebook' code actions") }); + try { + const cell = notebookModel.cells[0]; const ref = await this.textModelService.createModelReference(cell.uri); - disposable.add(ref); + nbDisposable.add(ref); const textEditorModel = ref.object.textEditorModel; - await this.applyOnSaveActions(textEditorModel, editorCodeActionsOnSave, excludedActions, progress, token); - })); - } catch { - this.logService.error('Failed to apply code action on save'); - } finally { - progress.report({ increment: 100 }); - disposable.dispose(); + await this.instantiationService.invokeFunction(CodeActionParticipantUtils.applyOnSaveGenericCodeActions, textEditorModel, notebookCodeActionsOnSave, excludedActions, progress, token); + } catch { + this.logService.error('Failed to apply notebook code action on save'); + } finally { + progress.report({ increment: 100 }); + nbDisposable.dispose(); + } + } + + // run cell level code actions + if (editorCodeActionsOnSave.length) { + // prioritize `source.fixAll` code actions + if (!Array.isArray(setting)) { + editorCodeActionsOnSave.sort((a, b) => { + if (CodeActionKind.SourceFixAll.contains(a)) { + if (CodeActionKind.SourceFixAll.contains(b)) { + return 0; + } + return -1; + } + if (CodeActionKind.SourceFixAll.contains(b)) { + return 1; + } + return 0; + }); + } + + const cellDisposable = new DisposableStore(); + progress.report({ message: localize('notebookSaveParticipants.cellCodeActions', "Running 'Cell' code actions") }); + try { + await Promise.all(notebookModel.cells.map(async cell => { + const ref = await this.textModelService.createModelReference(cell.uri); + cellDisposable.add(ref); + + const textEditorModel = ref.object.textEditorModel; + + await this.instantiationService.invokeFunction(CodeActionParticipantUtils.applyOnSaveGenericCodeActions, textEditorModel, editorCodeActionsOnSave, excludedActions, progress, token); + })); + } catch { + this.logService.error('Failed to apply code action on save'); + } finally { + progress.report({ increment: 100 }); + cellDisposable.dispose(); + } } } @@ -433,8 +431,52 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa return kinds.every(otherKind => otherKind.equals(kind) || !otherKind.contains(kind)); }); } +} - private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly HierarchicalKind[], excludes: readonly HierarchicalKind[], progress: IProgress, token: CancellationToken): Promise { +class CodeActionParticipantUtils { + + static async checkAndRunFormatCodeAction( + accessor: ServicesAccessor, + notebookModel: NotebookTextModel, + progress: IProgress, + token: CancellationToken): Promise { + + const instantiationService: IInstantiationService = accessor.get(IInstantiationService); + const textModelService: ITextModelService = accessor.get(ITextModelService); + const logService: ILogService = accessor.get(ILogService); + const configurationService: IConfigurationService = accessor.get(IConfigurationService); + + const formatDisposable = new DisposableStore(); + let formatResult: boolean = false; + progress.report({ message: localize('notebookSaveParticipants.formatCodeActions', "Running 'Format' code actions") }); + try { + const cell = notebookModel.cells[0]; + const ref = await textModelService.createModelReference(cell.uri); + formatDisposable.add(ref); + const textEditorModel = ref.object.textEditorModel; + + const defaultFormatterExtId = configurationService.getValue(NotebookSetting.defaultFormatter); + formatResult = await instantiationService.invokeFunction(CodeActionParticipantUtils.applyOnSaveFormatCodeAction, textEditorModel, new HierarchicalKind('notebook.format'), [], defaultFormatterExtId, progress, token); + } catch { + logService.error('Failed to apply notebook format action on save'); + } finally { + progress.report({ increment: 100 }); + formatDisposable.dispose(); + } + return formatResult; + } + + static async applyOnSaveGenericCodeActions( + accessor: ServicesAccessor, + model: ITextModel, + codeActionsOnSave: readonly HierarchicalKind[], + excludes: readonly HierarchicalKind[], + progress: IProgress, + token: CancellationToken): Promise { + + const instantiationService: IInstantiationService = accessor.get(IInstantiationService); + const languageFeaturesService: ILanguageFeaturesService = accessor.get(ILanguageFeaturesService); + const logService: ILogService = accessor.get(ILogService); const getActionProgress = new class implements IProgress { private _names = new Set(); @@ -444,7 +486,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa { key: 'codeaction.get2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] }, "Getting code actions from '{0}' ([configure]({1})).", [...this._names].map(name => `'${name}'`).join(', '), - 'command:workbench.action.openSettings?%5B%22editor.codeActionsOnSave%22%5D' + 'command:workbench.action.openSettings?%5B%22notebook.codeActionsOnSave%22%5D' ) }); } @@ -457,7 +499,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa }; for (const codeActionKind of codeActionsOnSave) { - const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, getActionProgress, token); + const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, languageFeaturesService, getActionProgress, token); if (token.isCancellationRequested) { actionsToRun.dispose(); return; @@ -480,11 +522,11 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa } } if (breakFlag) { - this.logService.warn('Failed to apply code action on save, applied to multiple resources.'); + logService.warn('Failed to apply code action on save, applied to multiple resources.'); continue; } progress.report({ message: localize('codeAction.apply', "Applying code action '{0}'.", action.action.title) }); - await this.instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token); + await instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token); if (token.isCancellationRequested) { return; } @@ -497,13 +539,79 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa } } - private getActionsToRun(model: ITextModel, codeActionKind: HierarchicalKind, excludes: readonly HierarchicalKind[], progress: IProgress, token: CancellationToken) { - return getCodeActions(this.languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { + static async applyOnSaveFormatCodeAction( + accessor: ServicesAccessor, + model: ITextModel, + formatCodeActionOnSave: HierarchicalKind, + excludes: readonly HierarchicalKind[], + extensionId: string | undefined, + progress: IProgress, + token: CancellationToken): Promise { + + const instantiationService: IInstantiationService = accessor.get(IInstantiationService); + const languageFeaturesService: ILanguageFeaturesService = accessor.get(ILanguageFeaturesService); + const logService: ILogService = accessor.get(ILogService); + + const getActionProgress = new class implements IProgress { + private _names = new Set(); + private _report(): void { + progress.report({ + message: localize( + { key: 'codeaction.get2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] }, + "Getting code actions from '{0}' ([configure]({1})).", + [...this._names].map(name => `'${name}'`).join(', '), + 'command:workbench.action.openSettings?%5B%22notebook.defaultFormatter%22%5D' + ) + }); + } + report(provider: CodeActionProvider) { + if (provider.displayName && !this._names.has(provider.displayName)) { + this._names.add(provider.displayName); + this._report(); + } + } + }; + + const providedActions = await CodeActionParticipantUtils.getActionsToRun(model, formatCodeActionOnSave, excludes, languageFeaturesService, getActionProgress, token); + // warn the user if there are more than one provided format action, and there is no specified defaultFormatter + if (providedActions.validActions.length > 1 && !extensionId) { + logService.warn('More than one format code action is provided, the 0th one will be used. A default can be specified via `notebook.defaultFormatter` in your settings.'); + } + + if (token.isCancellationRequested) { + providedActions.dispose(); + return false; + } + + try { + const action: CodeActionItem | undefined = extensionId ? providedActions.validActions.find(action => action.provider?.extensionId === extensionId) : providedActions.validActions[0]; + if (!action) { + return false; + } + + progress.report({ message: localize('codeAction.apply', "Applying code action '{0}'.", action.action.title) }); + await instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token); + if (token.isCancellationRequested) { + return false; + } + } catch { + logService.error('Failed to apply notebook format code action on save'); + return false; + } finally { + providedActions.dispose(); + } + return true; + } + + // @Yoyokrazy this could likely be modified to leverage the extensionID, therefore not getting actions from providers unnecessarily -- future work + static getActionsToRun(model: ITextModel, codeActionKind: HierarchicalKind, excludes: readonly HierarchicalKind[], languageFeaturesService: ILanguageFeaturesService, progress: IProgress, token: CancellationToken) { + return getCodeActions(languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { type: CodeActionTriggerType.Invoke, triggerAction: CodeActionTriggerSource.OnSave, filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, }, progress, token); } + } function getActiveCellCodeEditor(editorService: IEditorService): ICodeEditor | undefined { 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 f640be27a6f..7289f19d20b 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -15,8 +15,8 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseFeedbackKind, 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_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_NOTEBOOK_CELL_CHAT_FOCUSED, CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST, CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION, CTX_NOTEBOOK_CHAT_USER_DID_EDIT, MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; import { 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'; @@ -299,69 +299,6 @@ registerAction2(class extends NotebookAction { } }); -registerAction2(class extends NotebookAction { - constructor() { - super({ - id: 'notebook.cell.feedbackHelpful', - title: localize('feedback.helpful', 'Helpful'), - icon: Codicon.thumbsup, - menu: { - id: MENU_CELL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 1, - when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, - f1: false - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { - NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Helpful); - } -}); - -registerAction2(class extends NotebookAction { - constructor() { - super({ - id: 'notebook.cell.feedbackUnhelpful', - title: localize('feedback.unhelpful', 'Unhelpful'), - icon: Codicon.thumbsdown, - menu: { - id: MENU_CELL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 2, - when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, - f1: false - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { - NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Unhelpful); - } -}); - -registerAction2(class extends NotebookAction { - constructor() { - super({ - id: 'notebook.cell.reportIssueForBug', - title: localize('feedback.reportIssueForBug', 'Report Issue'), - icon: Codicon.report, - menu: { - id: MENU_CELL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 3, - when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, - f1: false - }); - } - - async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { - NotebookChatController.get(context.notebookEditor)?.feedbackLast(InlineChatResponseFeedbackKind.Bug); - } -}); - interface IInsertCellWithChatArgs extends INotebookActionContext { input?: string; autoSend?: boolean; 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 1df296db174..e5c83779451 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -4,18 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { Dimension, IFocusTracker, WindowIntervalTimer, getWindow, scheduleAtNextAnimationFrame, trackFocus } from 'vs/base/browser/dom'; -import { CancelablePromise, Queue, createCancelablePromise, disposableTimeout, raceCancellationError } from 'vs/base/common/async'; +import { CancelablePromise, DeferredPromise, Queue, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Emitter, Event } from 'vs/base/common/event'; -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { MovingAverage } from 'vs/base/common/numbers'; +import { isEqual } from 'vs/base/common/resources'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; @@ -27,24 +26,21 @@ 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 { ICommandService } from 'vs/platform/commands/common/commands'; +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 { AsyncProgress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { SaveReason } from 'vs/workbench/common/editor'; -import { GeneratingPhrase } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -import { IInlineChatSavingService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSavingService'; -import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { IInlineChatMessageAppender, InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { asProgressiveEdit, performAsyncTextEdit } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, EditMode, IInlineChatProgressItem, IInlineChatRequest, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +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_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; +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'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -85,17 +81,34 @@ class NotebookChatWidget extends Disposable implements INotebookViewZone { ) { super(); - this._register(inlineChatWidget.onDidChangeHeight(() => { + const updateHeight = () => { + if (this.heightInPx === inlineChatWidget.contentHeight) { + return; + } + this.heightInPx = inlineChatWidget.contentHeight; this._notebookEditor.changeViewZones(accessor => { accessor.layoutZone(id); }); this._layoutWidget(inlineChatWidget, widgetContainer); + }; + + this._register(inlineChatWidget.onDidChangeHeight(() => { + updateHeight(); })); + this._register(inlineChatWidget.chatWidget.onDidChangeHeight(() => { + updateHeight(); + })); + + this.heightInPx = inlineChatWidget.contentHeight; this._layoutWidget(inlineChatWidget, widgetContainer); } + layout() { + this._layoutWidget(this.inlineChatWidget, this.widgetContainer); + } + restoreEditingCell(initEditingCell: ICellViewModel) { this._editingCell = initEditingCell; @@ -245,7 +258,6 @@ export class NotebookChatController extends Disposable implements INotebookEdito private _strategy: EditStrategy | undefined; private _sessionCtor: CancelablePromise | undefined; - private _activeSession?: Session; private _warmupRequestCts?: CancellationTokenSource; private _activeRequestCts?: CancellationTokenSource; private readonly _ctxHasActiveRequest: IContextKey; @@ -253,28 +265,26 @@ export class NotebookChatController extends Disposable implements INotebookEdito private readonly _ctxUserDidEdit: IContextKey; private readonly _ctxOuterFocusPosition: IContextKey<'above' | 'below' | ''>; private readonly _userEditingDisposables = this._register(new DisposableStore()); - private readonly _ctxLastResponseType: IContextKey; - private _widget: NotebookChatWidget | undefined; private readonly _widgetDisposableStore = this._register(new DisposableStore()); private _focusTracker: IFocusTracker | undefined; + private _widget: NotebookChatWidget | undefined; + + private readonly _model: MutableDisposable = this._register(new MutableDisposable()); constructor( private readonly _notebookEditor: INotebookEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ICommandService private readonly _commandService: ICommandService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService, @IModelService private readonly _modelService: IModelService, @ILanguageService private readonly _languageService: ILanguageService, @INotebookExecutionStateService private _executionStateService: INotebookExecutionStateService, @IStorageService private readonly _storageService: IStorageService, - + @IChatService private readonly _chatService: IChatService, + @IChatVariablesService private readonly _chatVariableService: IChatVariablesService, ) { super(); this._ctxHasActiveRequest = CTX_NOTEBOOK_CHAT_HAS_ACTIVE_REQUEST.bindTo(this._contextKeyService); this._ctxCellWidgetFocused = CTX_NOTEBOOK_CELL_CHAT_FOCUSED.bindTo(this._contextKeyService); - this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService); this._ctxUserDidEdit = CTX_NOTEBOOK_CHAT_USER_DID_EDIT.bindTo(this._contextKeyService); this._ctxOuterFocusPosition = CTX_NOTEBOOK_CHAT_OUTER_FOCUS_POSITION.bindTo(this._contextKeyService); @@ -291,6 +301,13 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._historyCandidate = ''; this._storageService.store(NotebookChatController._storageKey, JSON.stringify(NotebookChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER); }; + + this._register(this._chatVariableService.registerVariable( + { id: '_notebookChatInput', name: '_notebookChatInput', description: '', hidden: true }, + async (_message, _arg, model) => { + return this._widget?.parentEditor.getModel()?.uri; + } + )); } private _registerFocusTracker() { @@ -408,10 +425,15 @@ export class NotebookChatController extends Disposable implements INotebookEdito ChatAgentLocation.Notebook, { telemetrySource: 'notebook-generate-cell', - inputMenuId: MENU_CELL_CHAT_INPUT, - widgetMenuId: MENU_CELL_CHAT_WIDGET, + inputMenuId: MenuId.ChatExecute, + widgetMenuId: MENU_INLINE_CHAT_WIDGET, statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK + rendererOptions: { + renderTextEditsAsSummary: (uri) => { + return isEqual(uri, this._widget?.parentEditor.getModel()?.uri) + || isEqual(uri, this._notebookEditor.textModel?.uri); + } + } } )); inlineChatWidget.placeholder = localize('default.placeholder', "Ask a question"); @@ -455,16 +477,10 @@ export class NotebookChatController extends Disposable implements INotebookEdito }, 0, this._store); this._sessionCtor = createCancelablePromise(async token => { - + await this._startSession(token); if (fakeParentEditor.hasModel()) { - await this._startSession(fakeParentEditor, token); - this._warmupRequestCts = new CancellationTokenSource(); - this._startInitialFolowups(fakeParentEditor, this._warmupRequestCts.token); if (this._widget) { - this._widget.inlineChatWidget.placeholder = this._activeSession?.session.placeholder ?? localize('default.placeholder', "Ask a question"); - this._widget.inlineChatWidget.updateInfo(this._activeSession?.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); - this._widget.inlineChatWidget.updateSlashCommands(this._activeSession?.session.slashCommands ?? []); this._focusWidget(); } @@ -480,6 +496,18 @@ export class NotebookChatController extends Disposable implements INotebookEdito }); } + private async _startSession(token: CancellationToken) { + if (!this._model.value) { + this._model.value = this._chatService.startSession(ChatAgentLocation.Editor, token); + + if (!this._model.value) { + throw new Error('Failed to start chat session'); + } + } + + this._strategy = new EditStrategy(); + } + private _scrollWidgetIntoView(index: number) { if (index === 0 || this._notebookEditor.getLength() === 0) { // the cell is at the beginning of the notebook @@ -516,20 +544,19 @@ export class NotebookChatController extends Disposable implements INotebookEdito async acceptInput() { assertType(this._widget); await this._sessionCtor; - assertType(this._activeSession); - this._warmupRequestCts?.dispose(true); - this._warmupRequestCts = undefined; - this._activeSession.addInput(new SessionPrompt(this._widget.inlineChatWidget.value)); + assertType(this._model.value); + assertType(this._strategy); - assertType(this._activeSession.lastInput); - const value = this._activeSession.lastInput.value; + const model = this._model.value; + this._widget.inlineChatWidget.setChatModel(model); - this._historyUpdate(value); + const lastInput = this._widget.inlineChatWidget.value; + this._historyUpdate(lastInput); const editor = this._widget.parentEditor; - const model = editor.getModel(); + const textModel = editor.getModel(); - if (!editor.hasModel() || !model) { + if (!editor.hasModel() || !textModel) { return; } @@ -554,217 +581,97 @@ export class NotebookChatController extends Disposable implements INotebookEdito } this._ctxHasActiveRequest.set(true); - this._widget.inlineChatWidget.updateSlashCommands(this._activeSession.session.slashCommands ?? []); - this._widget?.inlineChatWidget.updateProgress(true); - - const request: IInlineChatRequest = { - requestId: generateUuid(), - prompt: value, - attempt: 0, - selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 }, - wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - live: true, - previewDocument: model.uri, - withIntentDetection: true, // TODO: don't hard code but allow in corresponding UI to run without intent detection? - }; - - //TODO: update progress in a newly inserted cell below the widget instead of the fake editor this._activeRequestCts?.cancel(); this._activeRequestCts = new CancellationTokenSource(); - const progressEdits: TextEdit[][] = []; - const progressiveEditsQueue = new Queue(); - const progressiveEditsClock = StopWatch.create(); - const progressiveEditsAvgDuration = new MovingAverage(); - const progressiveEditsCts = new CancellationTokenSource(this._activeRequestCts.token); - let progressiveChatResponse: IInlineChatMessageAppender | undefined; - const progress = new AsyncProgress(async data => { - // console.log('received chunk', data, request); - - if (this._activeRequestCts?.token.isCancellationRequested) { - return; - } - - if (data.message) { - this._widget?.inlineChatWidget.updateToolbar(false); - this._widget?.inlineChatWidget.updateInfo(data.message); - } - - if (data.edits?.length) { - if (!request.live) { - throw new Error('Progress in NOT supported in non-live mode'); - } - progressEdits.push(data.edits); - progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); - progressiveEditsClock.reset(); - - progressiveEditsQueue.queue(async () => { - // making changes goes into a queue because otherwise the async-progress time will - // influence the time it takes to receive the changes and progressive typing will - // become infinitely fast - await this._makeChanges(data.edits!, data.editsShouldBeInstant - ? undefined - : { duration: progressiveEditsAvgDuration.value, token: progressiveEditsCts.token } - ); - }); - } - - if (data.markdownFragment) { - if (!progressiveChatResponse) { - const message = { - message: new MarkdownString(data.markdownFragment, { supportThemeIcons: true, supportHtml: true, isTrusted: false }), - requestId: request.requestId, - }; - progressiveChatResponse = this._widget?.inlineChatWidget.updateChatMessage(message, true); - } else { - progressiveChatResponse.appendContent(data.markdownFragment); - } - } - }); - - const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, this._activeRequestCts.token); - let response: ReplyResponse | ErrorResponse | EmptyResponse; + const store = new DisposableStore(); try { - this._widget?.inlineChatWidget.updateChatMessage(undefined); - this._widget?.inlineChatWidget.updateFollowUps(undefined); - this._widget?.inlineChatWidget.updateProgress(true); - this._widget?.inlineChatWidget.updateInfo(!this._activeSession.lastExchange ? GeneratingPhrase + '\u2026' : ''); this._ctxHasActiveRequest.set(true); - const reply = await raceCancellationError(Promise.resolve(task), this._activeRequestCts.token); - if (progressiveEditsQueue.size > 0) { - // we must wait for all edits that came in via progress to complete - await Event.toPromise(progressiveEditsQueue.onDrained); - } - await progress.drain(); + const progressiveEditsQueue = new Queue(); + const progressiveEditsClock = StopWatch.create(); + const progressiveEditsAvgDuration = new MovingAverage(); + const progressiveEditsCts = new CancellationTokenSource(this._activeRequestCts.token); - if (!reply) { - response = new EmptyResponse(); - } else { - const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - const replyResponse = response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), progressEdits, request.requestId, undefined); - for (let i = progressEdits.length; i < replyResponse.allLocalEdits.length; i++) { - await this._makeChanges(replyResponse.allLocalEdits[i], undefined); - } + const responsePromise = new DeferredPromise(); + const response = await this._widget.inlineChatWidget.chatWidget.acceptInput(); + if (response) { + let lastLength = 0; - if (this._activeSession?.provider.provideFollowups) { - const followupCts = new CancellationTokenSource(); - const followups = await this._activeSession.provider.provideFollowups(this._activeSession.session, replyResponse.raw, followupCts.token); - if (followups && this._widget) { - const widget = this._widget; - widget.inlineChatWidget.updateFollowUps(followups, async followup => { - if (followup.kind === 'reply') { - widget.inlineChatWidget.value = followup.message; - this.acceptInput(); - } else { - await this.acceptSession(); - this._commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); - } - }); + store.add(response.onDidChange(e => { + if (response.isCanceled) { + progressiveEditsCts.cancel(); + responsePromise.complete(); + return; } - } - this._userEditingDisposables.clear(); - // monitor user edits - const editingCell = this._widget.getEditingCell(); - if (editingCell) { - this._userEditingDisposables.add(editingCell.model.onDidChangeContent(() => this._updateUserEditingState())); - this._userEditingDisposables.add(editingCell.model.onDidChangeLanguage(() => this._updateUserEditingState())); - this._userEditingDisposables.add(editingCell.model.onDidChangeMetadata(() => this._updateUserEditingState())); - this._userEditingDisposables.add(editingCell.model.onDidChangeInternalMetadata(() => this._updateUserEditingState())); - this._userEditingDisposables.add(editingCell.model.onDidChangeOutputs(() => this._updateUserEditingState())); - this._userEditingDisposables.add(this._executionStateService.onDidChangeExecution(e => { - if (e.type === NotebookExecutionType.cell && e.affectsCell(editingCell.uri)) { - this._updateUserEditingState(); + if (response.isComplete) { + responsePromise.complete(); + return; + } + + const edits = response.response.value.map(part => { + if (part.kind === 'textEditGroup' + // && isEqual(part.uri, this._session?.textModelN.uri) + ) { + return part.edits; + } else { + return []; } - })); - } + }).flat(); + + const newEdits = edits.slice(lastLength); + // console.log('NEW edits', newEdits, edits); + if (newEdits.length === 0) { + return; // NO change + } + lastLength = edits.length; + progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); + progressiveEditsClock.reset(); + + progressiveEditsQueue.queue(async () => { + for (const edits of newEdits) { + await this._makeChanges(edits, { + duration: progressiveEditsAvgDuration.value, + token: progressiveEditsCts.token + }); + } + }); + })); + } + + await responsePromise.p; + await progressiveEditsQueue.whenIdle(); + + this._userEditingDisposables.clear(); + // monitor user edits + const editingCell = this._widget.getEditingCell(); + if (editingCell) { + this._userEditingDisposables.add(editingCell.model.onDidChangeContent(() => this._updateUserEditingState())); + this._userEditingDisposables.add(editingCell.model.onDidChangeLanguage(() => this._updateUserEditingState())); + this._userEditingDisposables.add(editingCell.model.onDidChangeMetadata(() => this._updateUserEditingState())); + this._userEditingDisposables.add(editingCell.model.onDidChangeInternalMetadata(() => this._updateUserEditingState())); + this._userEditingDisposables.add(editingCell.model.onDidChangeOutputs(() => this._updateUserEditingState())); + this._userEditingDisposables.add(this._executionStateService.onDidChangeExecution(e => { + if (e.type === NotebookExecutionType.cell && e.affectsCell(editingCell.uri)) { + this._updateUserEditingState(); + } + })); } } catch (e) { - response = new ErrorResponse(e); } finally { + store.dispose(); + this._ctxHasActiveRequest.set(false); - this._widget?.inlineChatWidget.updateProgress(false); - this._widget?.inlineChatWidget.updateInfo(''); - this._widget?.inlineChatWidget.updateToolbar(true); - } - - this._ctxHasActiveRequest.set(false); - this._widget?.inlineChatWidget.updateProgress(false); - this._widget?.inlineChatWidget.updateInfo(''); - this._widget?.inlineChatWidget.updateToolbar(true); - - this._activeSession?.addExchange(new SessionExchange(this._activeSession.lastInput, response)); - this._ctxLastResponseType.set(response instanceof ReplyResponse ? response.raw.type : undefined); - } - - private async _startSession(editor: IActiveCodeEditor, token: CancellationToken) { - if (this._activeSession) { - this._inlineChatSessionService.releaseSession(this._activeSession); - } - - const session = await this._inlineChatSessionService.createSession( - editor, - { editMode: EditMode.Live }, - token - ); - - if (!session) { - return; - } - - this._activeSession = session; - this._strategy = new EditStrategy(session); - } - - private async _startInitialFolowups(editor: IActiveCodeEditor, token: CancellationToken) { - if (!this._activeSession || !this._activeSession.provider.provideFollowups) { - return; - } - - const request: IInlineChatRequest = { - requestId: generateUuid(), - prompt: '', - attempt: 0, - selection: { selectionStartLineNumber: 1, selectionStartColumn: 1, positionLineNumber: 1, positionColumn: 1 }, - wholeRange: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - live: true, - previewDocument: editor.getModel().uri, - withIntentDetection: true - }; - - const progress = new AsyncProgress(async data => { }); - const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, progress, token); - const reply = await raceCancellationError(Promise.resolve(task), token); - if (token.isCancellationRequested) { - return; - } - - if (!reply) { - return; - } - - const markdownContents = new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false }); - const response = this._instantiationService.createInstance(ReplyResponse, reply, markdownContents, this._activeSession.textModelN.uri, this._activeSession.textModelN.getAlternativeVersionId(), [], request.requestId, undefined); - const followups = await this._activeSession.provider.provideFollowups(this._activeSession.session, response.raw, token); - if (followups && this._widget) { - const widget = this._widget; - widget.inlineChatWidget.updateFollowUps(followups, async followup => { - if (followup.kind === 'reply') { - widget.inlineChatWidget.value = followup.message; - this.acceptInput(); - } else { - await this.acceptSession(); - this._commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); - } - }); + this._widget.inlineChatWidget.updateProgress(false); + this._widget.inlineChatWidget.updateInfo(''); + this._widget.inlineChatWidget.updateToolbar(true); } } private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined) { - assertType(this._activeSession); assertType(this._strategy); assertType(this._widget); @@ -787,18 +694,13 @@ export class NotebookChatController extends Disposable implements INotebookEdito const actualEdits = !opts && moreMinimalEdits ? moreMinimalEdits : edits; const editOperations = actualEdits.map(TextEdit.asEditOperation); - this._inlineChatSavingService.markChanged(this._activeSession); try { - // this._ignoreModelContentChanged = true; - this._activeSession.wholeRange.trackEdits(editOperations); if (opts) { await this._strategy.makeProgressiveChanges(editor, editOperations, opts); } else { await this._strategy.makeChanges(editor, editOperations); } - // this._ctxDidEdit.set(this._activeSession.hasChangedText); } finally { - // this._ignoreModelContentChanged = false; } } @@ -807,7 +709,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito } async acceptSession() { - assertType(this._activeSession); + assertType(this._model); assertType(this._strategy); const editor = this._widget?.parentEditor; @@ -817,16 +719,16 @@ export class NotebookChatController extends Disposable implements INotebookEdito const editingCell = this._widget?.getEditingCell(); - if (editingCell && this._notebookEditor.hasModel() && this._activeSession.lastInput) { + if (editingCell && this._notebookEditor.hasModel()) { const cellId = NotebookCellTextModelLikeId.str({ uri: editingCell.uri, viewType: this._notebookEditor.textModel.viewType }); - const prompt = this._activeSession.lastInput.value; - this._promptCache.set(cellId, prompt); + if (this._widget?.inlineChatWidget.value) { + this._promptCache.set(cellId, this._widget.inlineChatWidget.value); + } this._onDidChangePromptCache.fire({ cell: editingCell.uri }); } try { - await this._strategy.apply(editor); - this._inlineChatSessionService.releaseSession(this._activeSession); + this._model.clear(); } catch (_err) { } this.dismiss(false); @@ -925,10 +827,6 @@ export class NotebookChatController extends Disposable implements INotebookEdito } async cancelCurrentRequest(discard: boolean) { - if (discard) { - this._strategy?.cancel(); - } - this._activeRequestCts?.cancel(); } @@ -937,20 +835,11 @@ export class NotebookChatController extends Disposable implements INotebookEdito } discard() { - this._strategy?.cancel(); this._activeRequestCts?.cancel(); this._widget?.discardChange(); this.dismiss(true); } - async feedbackLast(kind: InlineChatResponseFeedbackKind) { - if (this._activeSession?.lastExchange && this._activeSession.lastExchange.response instanceof ReplyResponse) { - this._activeSession.provider.handleInlineChatResponseFeedback?.(this._activeSession.session, this._activeSession.lastExchange.response.raw, kind); - this._widget?.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); - } - } - - dismiss(discard: boolean) { const widget = this._widget; const widgetIndex = widget?.afterModelPosition; @@ -977,6 +866,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._ctxUserDidEdit.set(false); this._sessionCtor?.cancel(); this._sessionCtor = undefined; + this._model.clear(); this._widget?.dispose(); this._widget = undefined; this._widgetDisposableStore.clear(); @@ -1012,10 +902,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito export class EditStrategy { private _editCount: number = 0; - constructor( - protected readonly _session: Session, - ) { - + constructor() { } async makeProgressiveChanges(editor: IActiveCodeEditor, edits: ISingleEditOperation[], opts: ProgressingEditsOptions): Promise { @@ -1049,37 +936,6 @@ export class EditStrategy { } editor.executeEdits('inline-chat-live', edits, cursorStateComputerAndInlineDiffCollection); } - - async apply(editor: IActiveCodeEditor) { - if (this._editCount > 0) { - editor.pushUndoStop(); - } - if (!(this._session.lastExchange?.response instanceof ReplyResponse)) { - return; - } - const { untitledTextModel } = this._session.lastExchange.response; - if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) { - await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); - } - } - - async cancel() { - const { textModelN: modelN, textModelNAltVersion, textModelNSnapshotAltVersion } = this._session; - if (modelN.isDisposed()) { - return; - } - - const targetAltVersion = textModelNSnapshotAltVersion ?? textModelNAltVersion; - while (targetAltVersion < modelN.getAlternativeVersionId() && modelN.canUndo()) { - modelN.undo(); - } - } - - createSnapshot(): void { - if (this._session && !this._session.textModel0.equalsTextBuffer(this._session.textModelN.getTextBuffer())) { - this._session.createSnapshot(); - } - } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 157c1a848af..67c09c27463 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -118,12 +118,11 @@ import { NotebookKernelHistoryService } from 'vs/workbench/contrib/notebook/brow import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/notebookLoggingService'; import { NotebookLoggingService } from 'vs/workbench/contrib/notebook/browser/services/notebookLoggingServiceImpl'; import product from 'vs/platform/product/common/product'; -import { NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { runAccessibilityHelpAction, showAccessibleOutput } from 'vs/workbench/contrib/notebook/browser/notebookAccessibility'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { NotebookVariables } from 'vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariables'; +import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { NotebookAccessibilityHelp } from 'vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp'; +import { NotebookAccessibleView } from 'vs/workbench/contrib/notebook/browser/notebookAccessibleView'; +import { DefaultFormatter } from 'vs/workbench/contrib/format/browser/formatActionsMultiple'; /*--------------------------------------------------------------------------------------------- */ @@ -702,37 +701,6 @@ class NotebookLanguageSelectorScoreRefine { } } -class NotebookAccessibilityHelpContribution extends Disposable { - static ID: 'notebookAccessibilityHelpContribution'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(105, 'notebook', async accessor => { - const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() - || accessor.get(ICodeEditorService).getFocusedCodeEditor() - || accessor.get(IEditorService).activeEditorPane; - - if (activeEditor) { - runAccessibilityHelpAction(accessor, activeEditor); - } - }, NOTEBOOK_IS_ACTIVE_EDITOR)); - } -} - -class NotebookAccessibleViewContribution extends Disposable { - static ID: 'chatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(100, 'notebook', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const editorService = accessor.get(IEditorService); - - return showAccessibleOutput(accessibleViewService, editorService); - }, - ContextKeyExpr.and(NOTEBOOK_OUTPUT_FOCUSED, ContextKeyExpr.equals('resourceExtname', '.ipynb')) - )); - } -} - const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); registerWorkbenchContribution2(NotebookContribution.ID, NotebookContribution, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(CellContentProvider.ID, CellContentProvider, WorkbenchPhase.BlockStartup); @@ -741,10 +709,11 @@ registerWorkbenchContribution2(RegisterSchemasContribution.ID, RegisterSchemasCo registerWorkbenchContribution2(NotebookEditorManager.ID, NotebookEditorManager, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(NotebookLanguageSelectorScoreRefine.ID, NotebookLanguageSelectorScoreRefine, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(SimpleNotebookWorkingCopyEditorHandler.ID, SimpleNotebookWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); -workbenchContributionsRegistry.registerWorkbenchContribution(NotebookAccessibilityHelpContribution, LifecyclePhase.Eventually); -workbenchContributionsRegistry.registerWorkbenchContribution(NotebookAccessibleViewContribution, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookVariables, LifecyclePhase.Eventually); +AccessibleViewRegistry.register(new NotebookAccessibleView()); +AccessibleViewRegistry.register(new NotebookAccessibilityHelp()); + registerSingleton(INotebookService, NotebookService, InstantiationType.Delayed); registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl, InstantiationType.Delayed); registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverServiceImpl, InstantiationType.Delayed); @@ -973,6 +942,12 @@ configurationRegistry.registerConfiguration({ default: 0, tags: ['notebookLayout'] }, + [NotebookSetting.markdownLineHeight]: { + markdownDescription: nls.localize('notebook.markdown.lineHeight', "Controls the line height in pixels of markdown cells in notebooks. When set to {0}, {1} will be used", '`0`', '`normal`'), + type: 'number', + default: 0, + tags: ['notebookLayout'] + }, [NotebookSetting.cellEditorOptionsCustomizations]: editorOptionsCustomizationSchema, [NotebookSetting.interactiveWindowCollapseCodeCells]: { markdownDescription: nls.localize('notebook.interactiveWindow.collapseCodeCells', "Controls whether code cells in the interactive window are collapsed by default."), @@ -1009,6 +984,14 @@ configurationRegistry.registerConfiguration({ tags: ['notebookLayout', 'notebookOutputLayout'], default: false }, + [NotebookSetting.defaultFormatter]: { + description: nls.localize('notebookFormatter.default', "Defines a default notebook formatter which takes precedence over all other formatter settings. Must be the identifier of an extension contributing a formatter."), + type: ['string', 'null'], + default: null, + enum: DefaultFormatter.extensionIds, + enumItemLabels: DefaultFormatter.extensionItemLabels, + markdownEnumDescriptions: DefaultFormatter.extensionDescriptions + }, [NotebookSetting.formatOnSave]: { markdownDescription: nls.localize('notebook.formatOnSave', "Format a notebook on save. A formatter must be available, the file must not be saved after delay, and the editor must not be shutting down."), type: 'boolean', @@ -1073,9 +1056,10 @@ configurationRegistry.registerConfiguration({ tags: ['notebookLayout'] }, [NotebookSetting.remoteSaving]: { - markdownDescription: nls.localize('notebook.remoteSaving', "Enables the incremental saving of notebooks in Remote environment. When enabled, only the changes to the notebook are sent to the extension host, improving performance for large notebooks and slow network connections."), + 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', - default: typeof product.quality === 'string' && product.quality !== 'stable' // only enable as default in insiders + default: typeof product.quality === 'string' && product.quality !== 'stable', // only enable as default in insiders + tags: ['experimental'] }, [NotebookSetting.scrollToRevealCell]: { markdownDescription: nls.localize('notebook.scrolling.revealNextCellOnExecute.description', "How far to scroll when revealing the next cell upon running {0}.", 'notebook.cell.executeAndSelectBelow'), diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts index c6bb3937f91..11a10ed5950 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibility.ts @@ -3,127 +3,3 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { format } from 'vs/base/common/strings'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { IVisibleEditorPane } from 'vs/workbench/common/editor'; - -export function getAccessibilityHelpText(accessor: ServicesAccessor): string { - const keybindingService = accessor.get(IKeybindingService); - const content = []; - content.push(localize('notebook.overview', 'The notebook view is a collection of code and markdown cells. Code cells can be executed and will produce output directly below the cell.')); - content.push(descriptionForCommand('notebook.cell.edit', - localize('notebook.cell.edit', 'The Edit Cell command ({0}) will focus on the cell input.'), - localize('notebook.cell.editNoKb', 'The Edit Cell command will focus on the cell input and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.cell.quitEdit', - localize('notebook.cell.quitEdit', 'The Quit Edit command ({0}) will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.'), - localize('notebook.cell.quitEditNoKb', 'The Quit Edit command will set focus on the cell container and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.cell.focusInOutput', - localize('notebook.cell.focusInOutput', 'The Focus Output command ({0}) will set focus in the cell\'s output.'), - localize('notebook.cell.focusInOutputNoKb', 'The Quit Edit command will set focus in the cell\'s output and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.focusNextEditor', - localize('notebook.focusNextEditor', 'The Focus Next Cell Editor command ({0}) will set focus in the next cell\'s editor.'), - localize('notebook.focusNextEditorNoKb', 'The Focus Next Cell Editor command will set focus in the next cell\'s editor and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('notebook.focusPreviousEditor', - localize('notebook.focusPreviousEditor', 'The Focus Previous Cell Editor command ({0}) will set focus in the previous cell\'s editor.'), - localize('notebook.focusPreviousEditorNoKb', 'The Focus Previous Cell Editor command will set focus in the previous cell\'s editor and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(localize('notebook.cellNavigation', 'The up and down arrows will also move focus between cells while focused on the outer cell container.')); - content.push(descriptionForCommand('notebook.cell.executeAndFocusContainer', - localize('notebook.cell.executeAndFocusContainer', 'The Execute Cell command ({0}) executes the cell that currently has focus.',), - localize('notebook.cell.executeAndFocusContainerNoKb', 'The Execute Cell command executes the cell that currently has focus and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(localize('notebook.cell.insertCodeCellBelowAndFocusContainer', 'The Insert Cell Above/Below commands will create new empty code cells')); - content.push(localize('notebook.changeCellType', 'The Change Cell to Code/Markdown commands are used to switch between cell types.')); - - - return content.join('\n\n'); -} - -function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string { - const kb = keybindingService.lookupKeybinding(commandId); - if (kb) { - return format(msg, kb.getAriaLabel()); - } - return format(noKbMsg, commandId); -} - -export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | IVisibleEditorPane): Promise { - const accessibleViewService = accessor.get(IAccessibleViewService); - const helpText = getAccessibilityHelpText(accessor); - accessibleViewService.show({ - id: AccessibleViewProviderId.Notebook, - verbositySettingKey: AccessibilityVerbositySettingId.Notebook, - provideContent: () => helpText, - onClose: () => { - editor.focus(); - }, - options: { type: AccessibleViewType.Help } - }); -} - -export function showAccessibleOutput(accessibleViewService: IAccessibleViewService, editorService: IEditorService) { - const activePane = editorService.activeEditorPane; - const notebookEditor = getNotebookEditorFromEditorPane(activePane); - const notebookViewModel = notebookEditor?.getViewModel(); - const selections = notebookViewModel?.getSelections(); - const notebookDocument = notebookViewModel?.notebookDocument; - - if (!selections || !notebookDocument || !notebookEditor?.textModel) { - return false; - } - - const viewCell = notebookViewModel.viewCells[selections[0].start]; - let outputContent = ''; - const decoder = new TextDecoder(); - for (let i = 0; i < viewCell.outputsViewModels.length; i++) { - const outputViewModel = viewCell.outputsViewModels[i]; - const outputTextModel = viewCell.model.outputs[i]; - const [mimeTypes, pick] = outputViewModel.resolveMimeTypes(notebookEditor.textModel, undefined); - const mimeType = mimeTypes[pick].mimeType; - let buffer = outputTextModel.outputs.find(output => output.mime === mimeType); - - if (!buffer || mimeType.startsWith('image')) { - buffer = outputTextModel.outputs.find(output => !output.mime.startsWith('image')); - } - - let text = `${mimeType}`; // default in case we can't get the text value for some reason. - if (buffer) { - const charLimit = 100_000; - text = decoder.decode(buffer.data.slice(0, charLimit).buffer); - - if (buffer.data.byteLength > charLimit) { - text = text + '...(truncated)'; - } - - if (mimeType.endsWith('error')) { - text = text.replace(/\\u001b\[[0-9;]*m/gi, '').replaceAll('\\n', '\n'); - } - } - - const index = viewCell.outputsViewModels.length > 1 - ? `Cell output ${i + 1} of ${viewCell.outputsViewModels.length}\n` - : ''; - outputContent = outputContent.concat(`${index}${text}\n`); - } - - if (!outputContent) { - return false; - } - - accessibleViewService.show({ - id: AccessibleViewProviderId.Notebook, - verbositySettingKey: AccessibilityVerbositySettingId.Notebook, - provideContent(): string { return outputContent; }, - onClose() { - notebookEditor?.setFocus(selections[0]); - activePane?.focus(); - }, - options: { type: AccessibleViewType.View } - }); - return true; -} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts new file mode 100644 index 00000000000..c06912b28f9 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { localize } from 'vs/nls'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; + +export class NotebookAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 105; + readonly name = 'notebook'; + readonly when = NOTEBOOK_IS_ACTIVE_EDITOR; + readonly type: AccessibleViewType = AccessibleViewType.Help; + getProvider(accessor: ServicesAccessor) { + const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() + || accessor.get(ICodeEditorService).getFocusedCodeEditor() + || accessor.get(IEditorService).activeEditorPane; + + if (activeEditor) { + return runAccessibilityHelpAction(accessor, activeEditor); + } + return; + } +} + + + +export function getAccessibilityHelpText(): string { + return [ + localize('notebook.overview', 'The notebook view is a collection of code and markdown cells. Code cells can be executed and will produce output directly below the cell.'), + localize('notebook.cell.edit', 'The Edit Cell command will focus on the cell input.'), + localize('notebook.cell.quitEdit', 'The Quit Edit command will set focus on the cell container. The default (Escape) key may need to be pressed twice first exit the virtual cursor if active.'), + localize('notebook.cell.focusInOutput', 'The Focus Output command will set focus in the cell\'s output.'), + localize('notebook.focusNextEditor', 'The Focus Next Cell Editor command will set focus in the next cell\'s editor.'), + localize('notebook.focusPreviousEditor', 'The Focus Previous Cell Editor command will set focus in the previous cell\'s editor.'), + localize('notebook.cellNavigation', 'The up and down arrows will also move focus between cells while focused on the outer cell container.'), + localize('notebook.cell.executeAndFocusContainer', 'The Execute Cell command executes the cell that currently has focus.',), + localize('notebook.cell.insertCodeCellBelowAndFocusContainer', 'The Insert Cell Above/Below commands will create new empty code cells'), + localize('notebook.changeCellType', 'The Change Cell to Code/Markdown commands are used to switch between cell types.') + ].join('\n\n'); +} + +export function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor | IVisibleEditorPane) { + const helpText = getAccessibilityHelpText(); + return { + id: AccessibleViewProviderId.Notebook, + verbositySettingKey: AccessibilityVerbositySettingId.Notebook, + provideContent: () => helpText, + onClose: () => { + editor.focus(); + }, + options: { type: AccessibleViewType.Help } + }; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts new file mode 100644 index 00000000000..3975c17bb9e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class NotebookAccessibleView implements IAccessibleViewImplentation { + readonly priority = 100; + readonly name = 'notebook'; + readonly type = AccessibleViewType.View; + readonly when = ContextKeyExpr.and(NOTEBOOK_OUTPUT_FOCUSED, ContextKeyExpr.equals('resourceExtname', '.ipynb')); + getProvider(accessor: ServicesAccessor) { + const editorService = accessor.get(IEditorService); + return showAccessibleOutput(editorService); + } +} + + +export function showAccessibleOutput(editorService: IEditorService) { + const activePane = editorService.activeEditorPane; + const notebookEditor = getNotebookEditorFromEditorPane(activePane); + const notebookViewModel = notebookEditor?.getViewModel(); + const selections = notebookViewModel?.getSelections(); + const notebookDocument = notebookViewModel?.notebookDocument; + + if (!selections || !notebookDocument || !notebookEditor?.textModel) { + return; + } + + const viewCell = notebookViewModel.viewCells[selections[0].start]; + let outputContent = ''; + const decoder = new TextDecoder(); + for (let i = 0; i < viewCell.outputsViewModels.length; i++) { + const outputViewModel = viewCell.outputsViewModels[i]; + const outputTextModel = viewCell.model.outputs[i]; + const [mimeTypes, pick] = outputViewModel.resolveMimeTypes(notebookEditor.textModel, undefined); + const mimeType = mimeTypes[pick].mimeType; + let buffer = outputTextModel.outputs.find(output => output.mime === mimeType); + + if (!buffer || mimeType.startsWith('image')) { + buffer = outputTextModel.outputs.find(output => !output.mime.startsWith('image')); + } + + let text = `${mimeType}`; // default in case we can't get the text value for some reason. + if (buffer) { + const charLimit = 100_000; + text = decoder.decode(buffer.data.slice(0, charLimit).buffer); + + if (buffer.data.byteLength > charLimit) { + text = text + '...(truncated)'; + } + + if (mimeType.endsWith('error')) { + text = text.replace(/\\u001b\[[0-9;]*m/gi, '').replaceAll('\\n', '\n'); + } + } + + const index = viewCell.outputsViewModels.length > 1 + ? `Cell output ${i + 1} of ${viewCell.outputsViewModels.length}\n` + : ''; + outputContent = outputContent.concat(`${index}${text}\n`); + } + + if (!outputContent) { + return; + } + + return { + id: AccessibleViewProviderId.Notebook, + verbositySettingKey: AccessibilityVerbositySettingId.Notebook, + provideContent(): string { return outputContent; }, + onClose() { + notebookEditor?.setFocus(selections[0]); + activePane?.focus(); + }, + options: { type: AccessibleViewType.View } + }; +} + diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 3d306304664..473e8a1168d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -430,6 +430,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane, I const startTime = perfMarks['startTime']; const extensionActivated = perfMarks['extensionActivated']; const inputLoaded = perfMarks['inputLoaded']; + const webviewCommLoaded = perfMarks['webviewCommLoaded']; const customMarkdownLoaded = perfMarks['customMarkdownLoaded']; const editorLoaded = perfMarks['editorLoaded']; @@ -444,7 +445,11 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane, I if (inputLoaded !== undefined) { inputLoadingTimespan = inputLoaded - extensionActivated; - webviewCommLoadingTimespan = inputLoaded - extensionActivated; // TODO@rebornix, we don't track webview comm anymore + } + + if (webviewCommLoaded !== undefined) { + webviewCommLoadingTimespan = webviewCommLoaded - extensionActivated; + } if (customMarkdownLoaded !== undefined) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 3a65e207a93..05156eebb72 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -366,6 +366,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD || e.dragAndDropEnabled || e.fontSize || e.markupFontSize + || e.markdownLineHeight || e.fontFamily || e.insertToolbarAlignment || e.outputFontSize @@ -1501,7 +1502,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); // init rendering - await this._warmupWithMarkdownRenderer(this.viewModel, viewState); + await this._warmupWithMarkdownRenderer(this.viewModel, viewState, perf); perf?.mark('customMarkdownLoaded'); @@ -1586,10 +1587,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._lastCellWithEditorFocus = cell; } - private async _warmupWithMarkdownRenderer(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined) { + private async _warmupWithMarkdownRenderer(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined, perf?: NotebookPerfMarks) { this.logService.debug('NotebookEditorWidget', 'warmup ' + this.viewModel?.uri.toString()); await this._resolveWebview(); + perf?.mark('webviewCommLoaded'); + this.logService.debug('NotebookEditorWidget', 'warmup - webview resolved'); // make sure that the webview is not visible otherwise users will see pre-rendered markdown cells in wrong position as the list view doesn't have a correct `top` offset yet diff --git a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts index 6cc994b7ff6..46ce122a004 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts @@ -47,6 +47,7 @@ export interface NotebookDisplayOptions { // TODO @Yoyokrazy rename to a more ge outputFontFamily: string; outputLineHeight: number; markupFontSize: number; + markdownLineHeight: number; editorOptionsCustomizations: Partial<{ 'editor.indentSize': 'tabSize' | number; 'editor.tabSize': number; @@ -96,6 +97,7 @@ export interface NotebookOptionsChangeEvent { readonly fontSize?: boolean; readonly outputFontSize?: boolean; readonly markupFontSize?: boolean; + readonly markdownLineHeight?: boolean; readonly fontFamily?: boolean; readonly outputFontFamily?: boolean; readonly editorOptionsCustomizations?: boolean; @@ -159,6 +161,7 @@ export class NotebookOptions extends Disposable { // const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(compactView, insertToolbarPosition, insertToolbarAlignment); const fontSize = this.configurationService.getValue('editor.fontSize'); const markupFontSize = this.configurationService.getValue(NotebookSetting.markupFontSize); + const markdownLineHeight = this.configurationService.getValue(NotebookSetting.markdownLineHeight); let editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.markupFontSize); } + if (markdownLineHeight) { + configuration.markdownLineHeight = this.configurationService.getValue(NotebookSetting.markdownLineHeight); + } + if (outputFontFamily) { configuration.outputFontFamily = this.configurationService.getValue(NotebookSetting.outputFontFamily); } @@ -579,6 +589,7 @@ export class NotebookOptions extends Disposable { fontSize, outputFontSize, markupFontSize, + markdownLineHeight, fontFamily, outputFontFamily, editorOptionsCustomizations, @@ -796,6 +807,7 @@ export class NotebookOptions extends Disposable { outputFontSize: this._layoutConfiguration.outputFontSize, outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + markdownLineHeight: this._layoutConfiguration.markdownLineHeight, outputLineHeight: this._layoutConfiguration.outputLineHeight, outputScrolling: this._layoutConfiguration.outputScrolling, outputWordWrap: this._layoutConfiguration.outputWordWrap, @@ -819,6 +831,7 @@ export class NotebookOptions extends Disposable { outputFontSize: this._layoutConfiguration.outputFontSize, outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + markdownLineHeight: this._layoutConfiguration.markdownLineHeight, outputLineHeight: this._layoutConfiguration.outputLineHeight, outputScrolling: this._layoutConfiguration.outputScrolling, outputWordWrap: this._layoutConfiguration.outputWordWrap, 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 86682ca341d..b8ccbf07abf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -84,7 +84,7 @@ export class CellComments extends CellContentPart { const layoutInfo = this.notebookEditor.getLayoutInfo(); - await this._commentThreadWidget.display(layoutInfo.fontInfo.lineHeight); + await this._commentThreadWidget.display(layoutInfo.fontInfo.lineHeight, true); this._applyTheme(); this.commentTheadDisposables.add(this._commentThreadWidget.onDidResize(() => { diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 48910a20dba..a7cd34ca453 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -306,6 +306,13 @@ export class NotebookCellList extends WorkbenchList implements ID return listView; } + /** + * Test Only + */ + _getView() { + return this.view; + } + attachWebview(element: HTMLElement) { element.style.top = `-${NOTEBOOK_WEBVIEW_BOUNDARY}px`; this.rowsContainer.insertAdjacentElement('afterbegin', element); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts index 399934d7c49..52fbff1055d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts @@ -189,7 +189,7 @@ export class NotebookCellsLayout implements IRangeMap { const index = afterPosition - 1; const previousItemPosition = this._prefixSumComputer.getPrefixSum(index); const previousItemSize = this._items[index].size; - const previousWhitespace = this._whitespace.filter(ws => ws.afterPosition === afterPosition - 1); + const previousWhitespace = this._whitespace.filter(ws => (ws.afterPosition <= afterPosition - 1 && ws.afterPosition > 0)); const whitespaceBefore = previousWhitespace.reduce((acc, ws) => acc + ws.size, 0); return previousItemPosition + previousItemSize + whitespaceBeforeFirstItem + this.paddingTop + whitespaceBefore; } @@ -290,8 +290,19 @@ export class NotebookCellListView extends ListView { } changeOneWhitespace(id: string, newAfterPosition: number, newSize: number) { - this.notebookRangeMap.changeOneWhitespace(id, newAfterPosition, newSize); - this.eventuallyUpdateScrollDimensions(); + const scrollTop = this.scrollTop; + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + const currentPosition = this.notebookRangeMap.getWhitespacePosition(id); + + if (currentPosition > scrollTop) { + this.notebookRangeMap.changeOneWhitespace(id, newAfterPosition, newSize); + this.render(previousRenderRange, scrollTop, this.lastRenderHeight, undefined, undefined, false); + this._rerender(scrollTop, this.renderHeight, false); + this.eventuallyUpdateScrollDimensions(); + } else { + this.notebookRangeMap.changeOneWhitespace(id, newAfterPosition, newSize); + this.eventuallyUpdateScrollDimensions(); + } } removeWhitespace(id: string): void { 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 cf7965b0752..702182063b3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -114,6 +114,7 @@ interface BacklayerWebviewOptions { readonly fontFamily: string; readonly outputFontFamily: string; readonly markupFontSize: number; + readonly markdownLineHeight: number; readonly outputLineHeight: number; readonly outputScrolling: boolean; readonly outputWordWrap: boolean; @@ -266,6 +267,7 @@ export class BackLayerWebView extends Themable { 'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`, 'notebook-markdown-min-height': `${this.options.previewNodePadding * 2}px`, 'notebook-markup-font-size': typeof this.options.markupFontSize === 'number' && this.options.markupFontSize > 0 ? `${this.options.markupFontSize}px` : `calc(${this.options.fontSize}px * 1.2)`, + '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`, @@ -366,6 +368,7 @@ export class BackLayerWebView extends Themable { white-space: initial; font-size: var(--notebook-markup-font-size); + line-height: var(--notebook-markdown-line-height); color: var(--theme-ui-foreground); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index e8ca556b48d..b3b6ff6e2f9 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -77,32 +77,38 @@ class StackOperation implements IWorkspaceUndoRedoElement { async undo(): Promise { this._pauseableEmitter.pause(); - for (let i = this._operations.length - 1; i >= 0; i--) { - await this._operations[i].undo(); + try { + for (let i = this._operations.length - 1; i >= 0; i--) { + await this._operations[i].undo(); + } + this._postUndoRedo(this._beginAlternativeVersionId); + this._pauseableEmitter.fire({ + rawEvents: [], + synchronous: undefined, + versionId: this.textModel.versionId, + endSelectionState: this._beginSelectionState + }); + } finally { + this._pauseableEmitter.resume(); } - this._postUndoRedo(this._beginAlternativeVersionId); - this._pauseableEmitter.fire({ - rawEvents: [], - synchronous: undefined, - versionId: this.textModel.versionId, - endSelectionState: this._beginSelectionState - }); - this._pauseableEmitter.resume(); } async redo(): Promise { this._pauseableEmitter.pause(); - for (let i = 0; i < this._operations.length; i++) { - await this._operations[i].redo(); + try { + for (let i = 0; i < this._operations.length; i++) { + await this._operations[i].redo(); + } + this._postUndoRedo(this._resultAlternativeVersionId); + this._pauseableEmitter.fire({ + rawEvents: [], + synchronous: undefined, + versionId: this.textModel.versionId, + endSelectionState: this._resultSelectionState + }); + } finally { + this._pauseableEmitter.resume(); } - this._postUndoRedo(this._resultAlternativeVersionId); - this._pauseableEmitter.fire({ - rawEvents: [], - synchronous: undefined, - versionId: this.textModel.versionId, - endSelectionState: this._resultSelectionState - }); - this._pauseableEmitter.resume(); } } @@ -146,6 +152,10 @@ type TransformedEdit = { }; class NotebookEventEmitter extends PauseableEmitter { + get isEmpty() { + return this._eventQueue.isEmpty(); + } + isDirtyEvent() { for (const e of this._eventQueue) { for (let i = 0; i < e.rawEvents.length; i++) { @@ -513,15 +523,17 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._doApplyEdits(rawEdits, synchronous, computeUndoRedo, beginSelectionState, undoRedoGroup); return true; } finally { - // Update selection and versionId after applying edits. - const endSelections = endSelectionsComputer(); - this._increaseVersionId(this._operationManager.isUndoStackEmpty() && !this._pauseableEmitter.isDirtyEvent()); + if (!this._pauseableEmitter.isEmpty) { + // Update selection and versionId after applying edits. + const endSelections = endSelectionsComputer(); + this._increaseVersionId(this._operationManager.isUndoStackEmpty() && !this._pauseableEmitter.isDirtyEvent()); - // Finalize undo element - this._operationManager.pushStackElement(this._alternativeVersionId, endSelections); + // Finalize undo element + this._operationManager.pushStackElement(this._alternativeVersionId, endSelections); - // Broadcast changes - this._pauseableEmitter.fire({ rawEvents: [], versionId: this.versionId, synchronous: synchronous, endSelectionState: endSelections }); + // Broadcast changes + this._pauseableEmitter.fire({ rawEvents: [], versionId: this.versionId, synchronous: synchronous, endSelectionState: endSelections }); + } this._pauseableEmitter.resume(); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 67315a1f315..7828091e598 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -790,7 +790,7 @@ export interface INotebookEditorModel extends IDisposable { readonly viewType: string; readonly notebook: INotebookTextModel | undefined; readonly hasErrorState: boolean; - isResolved(): this is IResolvedNotebookEditorModel; + isResolved(): boolean; isDirty(): boolean; isModified(): boolean; isReadonly(): boolean | IMarkdownString; @@ -932,6 +932,7 @@ export const NotebookSetting = { openGettingStarted: 'notebook.experimental.openGettingStarted', globalToolbarShowLabel: 'notebook.globalToolbarShowLabel', markupFontSize: 'notebook.markup.fontSize', + markdownLineHeight: 'notebook.markdown.lineHeight', interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode', outputScrollingDeprecated: 'notebook.experimental.outputScrolling', outputScrolling: 'notebook.output.scrolling', @@ -940,6 +941,7 @@ export const NotebookSetting = { minimalErrorRendering: 'notebook.output.minimalErrorRendering', formatOnSave: 'notebook.formatOnSave.enabled', insertFinalNewline: 'notebook.insertFinalNewline', + defaultFormatter: 'notebook.defaultFormatter', formatOnCellExecution: 'notebook.formatOnCellExecution', codeActionsOnSave: 'notebook.codeActionsOnSave', outputWordWrap: 'notebook.output.wordWrap', diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index be5a5ee4ed2..3c26eff229a 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 { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -195,7 +196,8 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF constructor( private readonly _notebookModel: NotebookTextModel, private readonly _notebookService: INotebookService, - private readonly _configurationService: IConfigurationService + private readonly _configurationService: IConfigurationService, + private readonly _telemetryService: ITelemetryService ) { super(); @@ -230,17 +232,41 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF // Override save behavior to avoid transferring the buffer across the wire 3 times if (saveWithReducedCommunication) { - this.save = async (options: IWriteFileOptions, token: CancellationToken) => { - const serializer = await this.getNotebookSerializer(); + this.setSaveDelegate().catch(console.error); + } + } - if (token.isCancellationRequested) { - throw new CancellationError(); - } + private async setSaveDelegate() { + const serializer = await this.getNotebookSerializer(); + this.save = async (options: IWriteFileOptions, token: CancellationToken) => { + if (token.isCancellationRequested) { + throw new CancellationError(); + } + try { const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token); return stat; - }; - } + } catch (error) { + if (!token.isCancellationRequested) { + type notebookSaveErrorData = { + isRemote: boolean; + error: Error; + }; + type notebookSaveErrorClassification = { + owner: 'amunger'; + comment: 'Detect if we are having issues saving a notebook on the Extension Host'; + isRemote: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether the save is happening on a remote file system' }; + error: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' }; + }; + this._telemetryService.publicLogError2('notebook/SaveError', { + isRemote: this._notebookModel.uri.scheme === Schemas.vscodeRemote, + error: error + }); + } + + throw error; + } + }; } override dispose(): void { @@ -332,6 +358,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo private readonly _viewType: string, @INotebookService private readonly _notebookService: INotebookService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService ) { } async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise { @@ -349,7 +376,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); + return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService, this._telemetryService); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index 0a8a17a170d..9d0a90e4576 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -22,6 +22,7 @@ import { assertIsDefined } from 'vs/base/common/types'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileReadLimits } from 'vs/platform/files/common/files'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; class NotebookModelReferenceCollection extends ReferenceCollection> { @@ -43,6 +44,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection>this._instantiationService.createInstance( FileWorkingCopyManager, workingCopyTypeId, 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 677f7f5b850..ef1c965adb2 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 @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { waitForState } from 'vs/base/common/observable'; @@ -15,12 +15,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IMarkerData, IMarkerService } from 'vs/platform/markers/common/markers'; -import { IInlineChatService, IInlineChatSessionProvider } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ChatAgentLocation, IChatAgent, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CellDiagnostics } from 'vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookCellExecution, INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { setupInstantiationService, TestNotebookExecutionStateService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; suite('notebookCellDiagnostics', () => { @@ -64,8 +65,26 @@ suite('notebookCellDiagnostics', () => { testExecutionService = new TestExecutionService(); instantiationService.stub(INotebookExecutionStateService, testExecutionService); - const chatProviders = instantiationService.get(IInlineChatService); - disposables.add(chatProviders.addProvider({} as IInlineChatSessionProvider)); + const agentData = { + extensionId: nullExtensionDescription.identifier, + extensionDisplayName: '', + extensionPublisherId: '', + name: 'testEditorAgent', + isDefault: true, + locations: [ChatAgentLocation.Editor], + metadata: {}, + slashCommands: [] + }; + const chatAgentService = new class extends mock() { + override getAgents(): IChatAgentData[] { + return [{ + id: 'testEditorAgent', + ...agentData + }]; + } + override onDidChangeAgents: Event = Event.None; + }; + instantiationService.stub(IChatAgentService, chatAgentService); markerService = new class extends mock() { override markers: ResourceMap = new ResourceMap(); 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 16f6b0728a3..071017d82d6 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -13,7 +13,9 @@ import { mock } from 'vs/base/test/common/mock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; 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 { 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'; import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; @@ -26,6 +28,7 @@ suite('NotebookFileWorkingCopyModel', function () { let disposables: DisposableStore; let instantiationService: TestInstantiationService; const configurationService = new TestConfigurationService(); + const telemetryService = new class extends mock() { }; teardown(() => disposables.dispose()); @@ -61,7 +64,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -83,7 +87,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -117,7 +122,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -139,7 +145,9 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService, + )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -172,7 +180,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); @@ -194,7 +203,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); await model.snapshot(SnapshotContext.Save, CancellationToken.None); assert.strictEqual(callCount, 1); @@ -228,7 +238,8 @@ suite('NotebookFileWorkingCopyModel', function () { } } ), - configurationService + configurationService, + telemetryService )); try { @@ -242,14 +253,57 @@ suite('NotebookFileWorkingCopyModel', function () { assert.strictEqual(callCount, 1); }); + + test('Notebook model will not return a save delegate if the serializer has not been retreived', async function () { + const notebook = instantiationService.createInstance(NotebookTextModel, + 'notebook', + URI.file('test'), + [{ cellKind: CellKind.Code, language: 'foo', mime: 'foo', source: 'foo', outputs: [], metadata: { foo: 123, bar: 456 } }], + {}, + { transientCellMetadata: {}, transientDocumentMetadata: {}, cellContentMetadata: {}, transientOutputs: false, } + ); + disposables.add(notebook); + + const serializer = new class extends mock() { + override save(): Promise { + return Promise.resolve({ name: 'savedFile' } as IFileStatWithMetadata); + } + }; + (serializer as any).test = 'yes'; + + let resolveSerializer: (serializer: INotebookSerializer) => void = () => { }; + const serializerPromise = new Promise(resolve => { + resolveSerializer = resolve; + }); + const notebookService = mockNotebookService(notebook, serializerPromise); + configurationService.setUserConfiguration(NotebookSetting.remoteSaving, true); + + const model = disposables.add(new NotebookFileWorkingCopyModel( + notebook, + notebookService, + configurationService, + telemetryService + )); + + // the save method should not be set if the serializer is not yet resolved + const notExist = model.save; + assert.strictEqual(notExist, undefined); + + resolveSerializer(serializer); + await model.getNotebookSerializer(); + const result = await model.save?.({} as any, {} as any); + + assert.strictEqual(result!.name, 'savedFile'); + }); }); -function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: INotebookSerializer) { +function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: Promise | INotebookSerializer) { return new class extends mock() { override async withNotebookDataProvider(viewType: string): Promise { + const serializer = await notebookSerializer; return new SimpleNotebookProviderInfo( notebook.viewType, - notebookSerializer, + serializer, { id: new ExtensionIdentifier('test'), location: undefined 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 bace7743e88..f2af8c32747 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookViewZones.test.ts @@ -511,6 +511,58 @@ suite('NotebookRangeMap with whitesspaces', () => { }); }); + test('Multiple Whitespaces 2', async function () { + await withTestNotebook( + [ + ['# header a', 'markdown', CellKind.Markup, [], {}], + ['var b = 1;', 'javascript', CellKind.Code, [], {}], + ['# header b', 'markdown', CellKind.Markup, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], {}], + ['# header c', 'markdown', CellKind.Markup, [], {}] + ], + async (editor, viewModel, disposables) => { + viewModel.restoreEditorViewState({ + editingCells: [false, false, false, false, false], + cellLineNumberStates: {}, + editorViewStates: [null, null, null, null, null], + cellTotalHeights: [50, 100, 50, 100, 50], + collapsedInputCells: {}, + collapsedOutputCells: {}, + }); + + const cellList = createNotebookCellList(instantiationService, disposables); + disposables.add(cellList); + cellList.attachViewModel(viewModel); + + // render height 210, it can render 3 full cells and 1 partial cell + cellList.layout(210, 100); + assert.strictEqual(cellList.scrollHeight, 350); + + cellList.changeViewZones(accessor => { + const first = accessor.addZone({ + afterModelPosition: 0, + heightInPx: 20, + domNode: document.createElement('div') + }); + accessor.layoutZone(first); + + const second = accessor.addZone({ + afterModelPosition: 1, + heightInPx: 20, + domNode: document.createElement('div') + }); + accessor.layoutZone(second); + + assert.strictEqual(cellList.scrollHeight, 390); + assert.strictEqual(cellList._getView().getWhitespacePosition(first), 0); + assert.strictEqual(cellList._getView().getWhitespacePosition(second), 70); + + accessor.removeZone(first); + accessor.removeZone(second); + }); + }); + }); + test('Whitespace with folding support', async function () { await withTestNotebook( [ diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 677afcd4ab8..6db24ac2b10 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -65,8 +65,6 @@ 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 { IInlineChatService } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl'; import { INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; @@ -201,7 +199,6 @@ 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(IInlineChatService, instantiationService.createInstance(InlineChatServiceImpl)); instantiationService.stub(INotebookCellOutlineProviderFactory, instantiationService.createInstance(NotebookCellOutlineProviderFactory)); instantiationService.stub(ILanguageDetectionService, new class MockLanguageDetectionService implements ILanguageDetectionService { diff --git a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts index 952b68dc613..276c5278bfe 100644 --- a/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts +++ b/src/vs/workbench/contrib/performance/electron-sandbox/startupTimings.ts @@ -20,6 +20,26 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { StartupTimings } from 'vs/workbench/contrib/performance/browser/startupTimings'; +import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { coalesce } from 'vs/base/common/arrays'; + +interface ITracingData { + readonly args?: { + readonly usedHeapSizeAfter?: number; + readonly usedHeapSizeBefore?: number; + }; + readonly dur: number; // in microseconds + readonly name: string; // e.g. MinorGC or MajorGC + readonly pid: number; +} + +interface IHeapStatistics { + readonly used: number; + readonly garbage: number; + readonly majorGCs: number; + readonly minorGCs: number; + readonly duration: number; +} export class NativeStartupTimings extends StartupTimings implements IWorkbenchContribution { @@ -34,7 +54,7 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo @IUpdateService updateService: IUpdateService, @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, @IProductService private readonly _productService: IProductService, - @IWorkspaceTrustManagementService workspaceTrustService: IWorkspaceTrustManagementService, + @IWorkspaceTrustManagementService workspaceTrustService: IWorkspaceTrustManagementService ) { super(editorService, paneCompositeService, lifecycleService, updateService, workspaceTrustService); @@ -62,10 +82,22 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo ]); const perfBaseline = await this._timerService.perfBaseline; + const heapStatistics = await this._resolveStartupHeapStatistics(); + if (heapStatistics) { + this._telemetryLogHeapStatistics(heapStatistics); + } if (appendTo) { - const content = `${this._timerService.startupMetrics.ellapsed}\t${this._productService.nameShort}\t${(this._productService.commit || '').slice(0, 10) || '0000000000'}\t${this._telemetryService.sessionId}\t${standardStartupError === undefined ? 'standard_start' : 'NO_standard_start : ' + standardStartupError}\t${String(perfBaseline).padStart(4, '0')}ms\n`; - await this.appendContent(URI.file(appendTo), content); + const content = coalesce([ + this._timerService.startupMetrics.ellapsed, + this._productService.nameShort, + (this._productService.commit || '').slice(0, 10) || '0000000000', + this._telemetryService.sessionId, + standardStartupError === undefined ? 'standard_start' : `NO_standard_start : ${standardStartupError}`, + `${String(perfBaseline).padStart(4, '0')}ms`, + heapStatistics ? this._printStartupHeapStatistics(heapStatistics) : undefined + ]).join('\t') + '\n'; + await this._appendContent(URI.file(appendTo), content); } if (durationMarkers?.length) { @@ -88,7 +120,7 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo const durationsContent = `${durations.join('\t')}\n`; if (durationMarkersFile) { - await this.appendContent(URI.file(durationMarkersFile), durationsContent); + await this._appendContent(URI.file(durationMarkersFile), durationsContent); } else { console.log(durationsContent); } @@ -109,7 +141,7 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo return super._isStandardStartup(); } - private async appendContent(file: URI, content: string): Promise { + private async _appendContent(file: URI, content: string): Promise { const chunks: VSBuffer[] = []; if (await this._fileService.exists(file)) { chunks.push((await this._fileService.readFile(file)).value); @@ -117,4 +149,88 @@ export class NativeStartupTimings extends StartupTimings implements IWorkbenchCo chunks.push(VSBuffer.fromString(content)); await this._fileService.writeFile(file, VSBuffer.concat(chunks)); } + + private async _resolveStartupHeapStatistics(): Promise { + if ( + !this._environmentService.args['enable-tracing'] || + !this._environmentService.args['trace-startup-file'] || + this._environmentService.args['trace-startup-format'] !== 'json' || + !this._environmentService.args['trace-startup-duration'] + ) { + return undefined; // unexpected arguments for startup heap statistics + } + + const used = (performance as unknown as { memory?: { usedJSHeapSize?: number } }).memory?.usedJSHeapSize ?? 0; // https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory + + let minorGCs = 0; + let majorGCs = 0; + let garbage = 0; + let duration = 0; + + try { + const traceContents: { traceEvents: ITracingData[] } = JSON.parse((await this._fileService.readFile(URI.file(this._environmentService.args['trace-startup-file']))).value.toString()); + for (const event of traceContents.traceEvents) { + if (event.pid !== process.pid) { + continue; + } + + switch (event.name) { + + // Major/Minor GC Events + case 'MinorGC': + minorGCs++; + case 'MajorGC': + majorGCs++; + if (event.args && typeof event.args.usedHeapSizeAfter === 'number' && typeof event.args.usedHeapSizeBefore === 'number') { + garbage += (event.args.usedHeapSizeBefore - event.args.usedHeapSizeAfter); + } + break; + + // GC Events that block the main thread + // Refs: https://v8.dev/blog/trash-talk + case 'V8.GCFinalizeMC': + case 'V8.GCScavenger': + duration += event.dur; + break; + } + } + + return { minorGCs, majorGCs, used, garbage, duration: Math.round(duration / 1000) }; + } catch (error) { + console.error(error); + } + + return undefined; + } + + private _telemetryLogHeapStatistics({ used, garbage, majorGCs, minorGCs, duration }: IHeapStatistics): void { + type StartupHeapStatisticsClassification = { + owner: 'bpasero'; + comment: 'An event that reports startup heap statistics for performance analysis.'; + heapUsed: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Used heap' }; + heapGarbage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Garbage heap' }; + majorGCs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Major GCs count' }; + minorGCs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Minor GCs count' }; + gcsDuration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'GCs duration' }; + }; + type StartupHeapStatisticsEvent = { + heapUsed: number; + heapGarbage: number; + majorGCs: number; + minorGCs: number; + gcsDuration: number; + }; + this._telemetryService.publicLog2('startupHeapStatistics', { + heapUsed: used, + heapGarbage: garbage, + majorGCs, + minorGCs, + gcsDuration: duration + }); + } + + private _printStartupHeapStatistics({ used, garbage, majorGCs, minorGCs, duration }: IHeapStatistics) { + const MB = 1024 * 1024; + return `Heap: ${Math.round(used / MB)}MB (used) ${Math.round(garbage / MB)}MB (garbage) ${majorGCs} (MajorGC) ${minorGCs} (MinorGC) ${duration}ms (GC duration)`; + } } diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 5e7f7f223d1..66ca47c40e5 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -46,6 +46,7 @@ import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/p import { IUserDataProfileService, CURRENT_PROFILE_CONTEXT } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -121,7 +122,7 @@ Registry.as(EditorExtensions.EditorFactory).registerEdit const OPEN_USER_SETTINGS_UI_TITLE = nls.localize2('openSettings2', "Open Settings (UI)"); const OPEN_USER_SETTINGS_JSON_TITLE = nls.localize2('openUserSettingsJson', "Open User Settings (JSON)"); const OPEN_APPLICATION_SETTINGS_JSON_TITLE = nls.localize2('openApplicationSettingsJson', "Open Application Settings (JSON)"); -const category = nls.localize2('preferences', "Preferences"); +const category = Categories.Preferences; interface IOpenSettingsActionOptions { openToSide?: boolean; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 078adf7dfa9..1ce23e61678 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -395,25 +395,31 @@ class EditSettingRenderer extends Disposable { private getActions(setting: IIndexedSetting, jsonSchema: IJSONSchema): IAction[] { if (jsonSchema.type === 'boolean') { - return [{ + return [{ id: 'truthyValue', label: 'true', + tooltip: 'true', enabled: true, - run: () => this.updateSetting(setting.key, true, setting) - }, { + run: () => this.updateSetting(setting.key, true, setting), + class: undefined + }, { id: 'falsyValue', label: 'false', + tooltip: 'false', enabled: true, - run: () => this.updateSetting(setting.key, false, setting) + run: () => this.updateSetting(setting.key, false, setting), + class: undefined }]; } if (jsonSchema.enum) { return jsonSchema.enum.map(value => { - return { + return { id: value, label: JSON.stringify(value), + tooltip: JSON.stringify(value), enabled: true, - run: () => this.updateSetting(setting.key, value, setting) + run: () => this.updateSetting(setting.key, value, setting), + class: undefined }; }); } @@ -423,11 +429,13 @@ class EditSettingRenderer extends Disposable { private getDefaultActions(setting: IIndexedSetting): IAction[] { if (this.isDefaultSettings()) { const settingInOtherModel = this.associatedPreferencesModel.getPreference(setting.key); - return [{ + return [{ id: 'setDefaultValue', label: settingInOtherModel ? nls.localize('replaceDefaultValue', "Replace in Settings") : nls.localize('copyDefaultValue', "Copy to Settings"), + tooltip: settingInOtherModel ? nls.localize('replaceDefaultValue', "Replace in Settings") : nls.localize('copyDefaultValue', "Copy to Settings"), enabled: true, - run: () => this.updateSetting(setting.key, setting.value, setting) + run: () => this.updateSetting(setting.key, setting.value, setting), + class: undefined }]; } return []; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 7649cc3acca..585b89b4014 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -185,11 +185,13 @@ export class FolderSettingsActionViewItem extends BaseActionViewItem { if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && workspaceFolders.length > 0) { actions.push(...workspaceFolders.map((folder, index) => { const folderCount = this._folderSettingCounts.get(folder.uri.toString()); - return { + return { id: 'folderSettingsTarget' + index, label: this.labelWithCount(folder.name, folderCount), - checked: this.folder && isEqual(this.folder.uri, folder.uri), + tooltip: this.labelWithCount(folder.name, folderCount), + checked: !!this.folder && isEqual(this.folder.uri, folder.uri), enabled: true, + class: undefined, run: () => this._action.run(folder) }; })); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 72f128ed81f..a47b4ff6e15 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -149,7 +149,7 @@ export const tocData: ITOCEntry = { { id: 'features/accessibilitySignals', label: localize('accessibility.signals', 'Accessibility Signals'), - settings: ['accessibility.signals.*', 'accessibility.signalOptions.*'] + settings: ['accessibility.signal*'] }, { id: 'features/accessibility', diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 24ecefc8617..80ce14bcaba 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -1711,12 +1711,12 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre // Use String constructor in case of null or undefined values const stringifiedDefaultValue = escapeInvisibleChars(String(dataElement.defaultValue)); - const displayOptions = settingEnum + const displayOptions: ISelectOptionItem[] = settingEnum .map(String) .map(escapeInvisibleChars) .map((data, index) => { const description = (enumDescriptions[index] && (enumDescriptionsAreMarkdown ? fixSettingLinks(enumDescriptions[index], false) : enumDescriptions[index])); - return { + return { text: enumItemLabels[index] ? enumItemLabels[index] : data, detail: enumItemLabels[index] ? data : '', description, @@ -1728,7 +1728,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre disposables: disposables }, decoratorRight: (((data === stringifiedDefaultValue) || (createdDefault && index === 0)) ? localize('settings.Default', "default") : '') - }; + } satisfies ISelectOptionItem; }); template.selectBox.setOptions(displayOptions); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 80736dc0e3b..4c9362f217c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -894,32 +894,35 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget this.editSetting(idx) }, - ] as IAction[]; + ]; if (item.removable) { actions.push({ class: ThemeIcon.asClassName(settingsRemoveIcon), enabled: true, id: 'workbench.action.removeListItem', + label: this.getLocalizedStrings().deleteActionTooltip, tooltip: this.getLocalizedStrings().deleteActionTooltip, run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) - } as IAction); + }); } else { actions.push({ class: ThemeIcon.asClassName(settingsDiscardIcon), enabled: true, id: 'workbench.action.resetListItem', + label: this.getLocalizedStrings().resetActionTooltip, tooltip: this.getLocalizedStrings().resetActionTooltip, run: () => this._onDidChangeList.fire({ originalItem: item, item: undefined, targetIndex: idx }) - } as IAction); + }); } return actions; diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 59ec31d81d6..17b7b7ac490 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -176,7 +176,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce const defaultAgent = this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel); if (defaultAgent) { additionalPicks.push({ - label: localize('askXInChat', "Ask {0}: {1}", defaultAgent.metadata.fullName, filter), + label: localize('askXInChat', "Ask {0}: {1}", defaultAgent.fullName, filter), commandId: this.configuration.experimental.askChatLocation === 'quickChat' ? ASK_QUICK_QUESTION_ACTION_ID : CHAT_OPEN_ACTION_ID, args: [filter] }); diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 088eec24d7c..7ee1d516f6d 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -6,7 +6,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 { Event } from 'vs/base/common/event'; +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'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -19,6 +19,8 @@ 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'; function getCount(repository: ISCMRepository): number { if (typeof repository.provider.count === 'number') { @@ -291,19 +293,17 @@ export class SCMActiveRepositoryContextKeyController implements IWorkbenchContri export class SCMActiveResourceContextKeyController implements IWorkbenchContribution { - private activeResourceHasChangesContextKey: IContextKey; - private activeResourceRepositoryContextKey: IContextKey; private readonly disposables = new DisposableStore(); private repositoryDisposables = new Set(); + private onDidRepositoryChange = new Emitter(); constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @IEditorService private readonly editorService: IEditorService, + @IEditorGroupsService editorGroupsService: IEditorGroupsService, @ISCMService private readonly scmService: ISCMService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { - this.activeResourceHasChangesContextKey = contextKeyService.createKey('scmActiveResourceHasChanges', false); - this.activeResourceRepositoryContextKey = contextKeyService.createKey('scmActiveResourceRepository', undefined); + 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); @@ -311,26 +311,42 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu this.onDidAddRepository(repository); } - editorService.onDidActiveEditorChange(this.updateContextKey, this, this.disposables); + // 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 + }; + + const repositoryContextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: activeResourceRepositoryContextKey, + getGroupContextKeyValue: (group) => this.getEditorRepositoryId(group.activeEditor), + onDidChange: this.onDidRepositoryChange.event + }; + + this.disposables.add(editorGroupsService.registerContextKeyProvider(hasChangesContextKeyProvider)); + this.disposables.add(editorGroupsService.registerContextKeyProvider(repositoryContextKeyProvider)); } private onDidAddRepository(repository: ISCMRepository): void { const onDidChange = Event.any(repository.provider.onDidChange, repository.provider.onDidChangeResources); - const changeDisposable = onDidChange(() => this.updateContextKey()); + 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.updateContextKey(); + this.onDidRepositoryChange.fire(); }); const disposable = combinedDisposable(changeDisposable, removeDisposable); this.repositoryDisposables.add(disposable); } - private updateContextKey(): void { - const activeResource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor); + 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( @@ -338,27 +354,37 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) ); - this.activeResourceRepositoryContextKey.set(activeResourceRepository?.id); + return activeResourceRepository?.id; + } + + return undefined; + } + + private getEditorHasChanges(activeEditor: EditorInput | null): boolean { + const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); + + if (activeResource?.scheme === Schemas.file || activeResource?.scheme === Schemas.vscodeRemote) { + const activeResourceRepository = Iterable.find( + this.scmService.repositories, + r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri)) + ); for (const resourceGroup of activeResourceRepository?.provider.groups ?? []) { if (resourceGroup.resources .some(scmResource => this.uriIdentityService.extUri.isEqual(activeResource, scmResource.sourceUri))) { - this.activeResourceHasChangesContextKey.set(true); - return; + return true; } } - - this.activeResourceHasChangesContextKey.set(false); - } else { - this.activeResourceHasChangesContextKey.set(false); - this.activeResourceRepositoryContextKey.set(undefined); } + + return false; } dispose(): void { this.disposables.dispose(); dispose(this.repositoryDisposables.values()); this.repositoryDisposables.clear(); + this.onDidRepositoryChange.dispose(); } } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 0796247d739..185aa10316f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/scm'; import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/resources'; -import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, IReference, DisposableMap } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, DisposableMap } from 'vs/base/common/lifecycle'; import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend, isPointerEvent } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; @@ -38,12 +38,11 @@ import { FileKind } from 'vs/platform/files/common/files'; import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { localize } from 'vs/nls'; +import { localize, localize2 } 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 { ITextModel } from 'vs/editor/common/model'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IModelService } from 'vs/editor/common/services/model'; @@ -62,7 +61,6 @@ import { LinkDetector } from 'vs/editor/contrib/links/browser/links'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILabelService } from 'vs/platform/label/common/label'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts'; @@ -86,7 +84,6 @@ import { MessageController } from 'vs/editor/contrib/message/browser/messageCont import { defaultButtonStyles, defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeActionController'; -import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { Schemas } from 'vs/base/common/network'; import { IDragAndDropData } from 'vs/base/browser/dnd'; import { fillEditorsDragData } from 'vs/workbench/browser/dnd'; @@ -111,6 +108,8 @@ import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/b import { IHoverService } from 'vs/platform/hover/browser/hover'; import { OpenScmGroupAction } from 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { ITextModel } from 'vs/editor/common/model'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; // type SCMResourceTreeNode = IResourceNode; // type SCMHistoryItemChangeResourceTreeNode = IResourceNode; @@ -225,7 +224,7 @@ export class ActionButtonRenderer implements ICompressibleTreeRenderer(); + private actionButtons = new Map(); constructor( @ICommandService private commandService: ICommandService, @@ -254,24 +253,8 @@ export class ActionButtonRenderer implements ICompressibleTreeRenderer { - const renderedActionButtons = this.actionButtons.get(actionButton) ?? []; - const renderedWidgetIndex = renderedActionButtons.findIndex(renderedActionButton => renderedActionButton === templateData.actionButton); - - if (renderedWidgetIndex < 0) { - throw new Error('Disposing unknown action button'); - } - - if (renderedActionButtons.length === 1) { - this.actionButtons.delete(actionButton); - } else { - renderedActionButtons.splice(renderedWidgetIndex, 1); - } - } - }); + this.actionButtons.set(actionButton, templateData.actionButton); + disposables.add({ dispose: () => this.actionButtons.delete(actionButton) }); templateData.disposable = disposables; } @@ -281,7 +264,7 @@ export class ActionButtonRenderer implements ICompressibleTreeRenderer renderedActionButton.focus()); + this.actionButtons.get(actionButton)?.focus(); } disposeElement(node: ITreeNode, index: number, template: ActionButtonTemplate): void { @@ -362,7 +345,7 @@ class InputRenderer implements ICompressibleTreeRenderer(); + private inputWidgets = new Map(); private contentHeights = new WeakMap(); private editorSelections = new WeakMap(); @@ -390,26 +373,12 @@ class InputRenderer implements ICompressibleTreeRenderer, index: number, templateData: InputTemplate): void { const input = node.element; - templateData.inputWidget.setInput(input); + templateData.inputWidget.input = input; // Remember widget - const renderedWidgets = this.inputWidgets.get(input) ?? []; - this.inputWidgets.set(input, [...renderedWidgets, templateData.inputWidget]); + this.inputWidgets.set(input, templateData.inputWidget); templateData.elementDisposables.add({ - dispose: () => { - const renderedWidgets = this.inputWidgets.get(input) ?? []; - const renderedWidgetIndex = renderedWidgets.findIndex(renderedWidget => renderedWidget === templateData.inputWidget); - - if (renderedWidgetIndex < 0) { - throw new Error('Disposing unknown input widget'); - } - - if (renderedWidgets.length === 1) { - this.inputWidgets.delete(input); - } else { - renderedWidgets.splice(renderedWidgetIndex, 1); - } - } + dispose: () => this.inputWidgets.delete(input) }); // Widget cursor selections @@ -472,16 +441,14 @@ class InputRenderer implements ICompressibleTreeRenderer { registerAction2(CollapseAllRepositoriesAction); registerAction2(ExpandAllRepositoriesAction); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.scm.action.focusInput', + title: { ...localize2('focusInput', "Focus Input") }, + category: localize2('source control', "Source Control"), + precondition: ContextKeys.RepositoryCount.notEqualsTo(0), + f1: true + }); + } + + override async run(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const scmView = await viewsService.openView(VIEW_PANE_ID); + if (scmView) { + scmView.focusInput(); + } + } +}); + const enum SCMInputWidgetCommandId { CancelAction = 'scm.input.cancelAction' } @@ -2280,7 +2265,7 @@ class SCMInputWidget { private toolbar: SCMInputWidgetToolbar; private readonly disposables = new DisposableStore(); - private model: { readonly input: ISCMInput; textModelRef?: IReference } | undefined; + private model: { readonly input: ISCMInput; readonly textModel: ITextModel } | undefined; private repositoryIdContextKey: IContextKey; private readonly repositoryDisposables = new DisposableStore(); @@ -2296,11 +2281,11 @@ class SCMInputWidget { readonly onDidChangeContentHeight: Event; - private get input(): ISCMInput | undefined { + get input(): ISCMInput | undefined { return this.model?.input; } - public async setInput(input: ISCMInput | undefined) { + set input(input: ISCMInput | undefined) { if (input === this.input) { return; } @@ -2312,34 +2297,18 @@ class SCMInputWidget { this.repositoryIdContextKey.set(input?.repository.id); if (!input) { - this.model?.textModelRef?.dispose(); this.inputEditor.setModel(undefined); this.model = undefined; return; } - const uri = input.repository.provider.inputBoxDocumentUri; - if (this.configurationService.getValue('editor.wordBasedSuggestions', { resource: uri }) !== 'off') { - this.configurationService.updateValue('editor.wordBasedSuggestions', 'off', { resource: uri }, ConfigurationTarget.MEMORY); - } - - const modelValue: typeof this.model = { input, textModelRef: undefined }; - - // Save model - this.model = modelValue; - - const modelRef = await this.textModelService.createModelReference(uri); - // Model has been changed in the meantime - if (this.model !== modelValue) { - modelRef.dispose(); - return; - } - - modelValue.textModelRef = modelRef; - - const textModel = modelRef.object.textEditorModel; + const textModel = input.repository.provider.inputBoxTextModel; this.inputEditor.setModel(textModel); + if (this.configurationService.getValue('editor.wordBasedSuggestions', { resource: textModel.uri }) !== 'off') { + this.configurationService.updateValue('editor.wordBasedSuggestions', 'off', { resource: textModel.uri }, ConfigurationTarget.MEMORY); + } + // Validation const validationDelayer = new ThrottledDelayer(200); const validate = async () => { @@ -2432,6 +2401,9 @@ class SCMInputWidget { // Toolbar this.toolbar.setInput(input); + + // Save model + this.model = { input, textModel }; } get selections(): Selection[] | null { @@ -2467,7 +2439,6 @@ class SCMInputWidget { overflowWidgetsDomNode: HTMLElement, @IContextKeyService contextKeyService: IContextKeyService, @IModelService private modelService: IModelService, - @ITextModelService private textModelService: ITextModelService, @IKeybindingService private keybindingService: IKeybindingService, @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -2738,7 +2709,7 @@ class SCMInputWidget { } dispose(): void { - this.setInput(undefined); + this.input = undefined; this.repositoryDisposables.dispose(); this.clearValidation(); this.disposables.dispose(); @@ -2880,7 +2851,6 @@ export class SCMViewPane extends ViewPane { this.storeTreeViewState(); }, this, this.disposables); - this.disposables.add(this.instantiationService.createInstance(ScmInputContentProvider)); Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire(), this, this.disposables); this.disposables.add(this.revealResourceThrottler); @@ -2929,45 +2899,49 @@ export class SCMViewPane extends ViewPane { this.onDidChangeBodyVisibility(async visible => { if (visible) { - await this.tree.setInput(this.scmViewService, viewState); + this.treeOperationSequencer.queue(async () => { + await this.tree.setInput(this.scmViewService, viewState); - Event.filter(this.configurationService.onDidChangeConfiguration, - e => - e.affectsConfiguration('scm.alwaysShowRepositories'), - this.visibilityDisposables) - (() => { - this.updateActions(); - this.updateChildren(); - }, this, this.visibilityDisposables); + Event.filter(this.configurationService.onDidChangeConfiguration, + e => + e.affectsConfiguration('scm.alwaysShowRepositories'), + this.visibilityDisposables) + (() => { + this.updateActions(); + this.updateChildren(); + }, this, this.visibilityDisposables); - Event.filter(this.configurationService.onDidChangeConfiguration, - e => - e.affectsConfiguration('scm.inputMinLineCount') || - e.affectsConfiguration('scm.inputMaxLineCount') || - e.affectsConfiguration('scm.showActionButton') || - e.affectsConfiguration('scm.showChangesSummary') || - e.affectsConfiguration('scm.showIncomingChanges') || - e.affectsConfiguration('scm.showOutgoingChanges'), - this.visibilityDisposables) - (() => this.updateChildren(), this, this.visibilityDisposables); + Event.filter(this.configurationService.onDidChangeConfiguration, + e => + e.affectsConfiguration('scm.inputMinLineCount') || + e.affectsConfiguration('scm.inputMaxLineCount') || + e.affectsConfiguration('scm.showActionButton') || + e.affectsConfiguration('scm.showChangesSummary') || + e.affectsConfiguration('scm.showIncomingChanges') || + e.affectsConfiguration('scm.showOutgoingChanges'), + this.visibilityDisposables) + (() => this.updateChildren(), this, this.visibilityDisposables); - // Add visible repositories - this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); - this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); - this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); + // Add visible repositories + this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); + this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); + this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); - // Restore scroll position - if (typeof this.treeScrollTop === 'number') { - this.tree.scrollTop = this.treeScrollTop; - this.treeScrollTop = undefined; - } + // Restore scroll position + if (typeof this.treeScrollTop === 'number') { + this.tree.scrollTop = this.treeScrollTop; + this.treeScrollTop = undefined; + } + + this.updateRepositoryCollapseAllContextKeys(); + }); } else { this.visibilityDisposables.clear(); this.onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); this.treeScrollTop = this.tree.scrollTop; - } - this.updateRepositoryCollapseAllContextKeys(); + this.updateRepositoryCollapseAllContextKeys(); + } }, this, this.disposables); this.disposables.add(this.instantiationService.createInstance(RepositoryVisibilityActionController)); @@ -3069,12 +3043,10 @@ export class SCMViewPane extends ViewPane { } else if (isSCMInput(e.element)) { this.scmViewService.focus(e.element.repository); - const widgets = this.inputRenderer.getRenderedInputWidget(e.element); + const widget = this.inputRenderer.getRenderedInputWidget(e.element); - if (widgets) { - for (const widget of widgets) { - widget.focus(); - } + if (widget) { + widget.focus(); this.tree.setFocus([], e.browserEvent); const selection = this.tree.getSelection(); @@ -3424,7 +3396,7 @@ export class SCMViewPane extends ViewPane { } if (focusedInput) { - this.inputRenderer.getRenderedInputWidget(focusedInput)?.forEach(widget => widget.focus()); + this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); } this.updateScmProviderContextKeys(); @@ -3481,6 +3453,19 @@ export class SCMViewPane extends ViewPane { } } + focusInput(): void { + this.treeOperationSequencer.queue(() => { + return new Promise(resolve => { + if (this.scmViewService.focusedRepository) { + this.tree.reveal(this.scmViewService.focusedRepository.input, 0.5); + this.inputRenderer.getRenderedInputWidget(this.scmViewService.focusedRepository.input)?.focus(); + } + + resolve(); + }); + }); + } + override shouldShowWelcome(): boolean { return this.scmService.repositoryCount === 0; } @@ -3492,22 +3477,26 @@ export class SCMViewPane extends ViewPane { override focus(): void { super.focus(); - if (this.isExpanded()) { - if (this.tree.getFocus().length === 0) { - for (const repository of this.scmViewService.visibleRepositories) { - const widgets = this.inputRenderer.getRenderedInputWidget(repository.input); + this.treeOperationSequencer.queue(() => { + return new Promise(resolve => { + if (this.isExpanded()) { + if (this.tree.getFocus().length === 0) { + for (const repository of this.scmViewService.visibleRepositories) { + const widget = this.inputRenderer.getRenderedInputWidget(repository.input); - if (widgets) { - for (const widget of widgets) { - widget.focus(); + if (widget) { + widget.focus(); + resolve(); + return; + } } - return; } - } - } - this.tree.domFocus(); - } + this.tree.domFocus(); + resolve(); + } + }); + }); } override dispose(): void { @@ -4002,23 +3991,3 @@ export class SCMActionButton implements IDisposable { } } } - -class ScmInputContentProvider extends Disposable implements ITextModelContentProvider { - - constructor( - @ITextModelService textModelService: ITextModelService, - @IModelService private readonly _modelService: IModelService, - @ILanguageService private readonly _languageService: ILanguageService, - ) { - super(); - this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeSourceControl, this)); - } - - async provideTextContent(resource: URI): Promise { - const existing = this._modelService.getModel(resource); - if (existing) { - return existing; - } - return this._modelService.createModel('', this._languageService.createById('scminput'), resource); - } -} diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 95bb756359d..3fe568b77d5 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -14,6 +14,7 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ResourceTree } from 'vs/base/common/resourceTree'; import { ISCMHistoryProvider, ISCMHistoryProviderMenus } from 'vs/workbench/contrib/scm/common/history'; +import { ITextModel } from 'vs/editor/common/model'; export const VIEWLET_ID = 'workbench.view.scm'; export const VIEW_PANE_ID = 'workbench.scm'; @@ -70,7 +71,7 @@ export interface ISCMProvider extends IDisposable { readonly onDidChangeResources: Event; readonly rootUri?: URI; - readonly inputBoxDocumentUri: URI; + readonly inputBoxTextModel: ITextModel; readonly count?: number; readonly commitTemplate: string; readonly historyProvider?: ISCMHistoryProvider; diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index 7e85cdaa4c1..762dc9ed1b6 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -5,7 +5,7 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation, ISCMActionButtonDescriptor } from './scm'; +import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator, ISCMInputChangeEvent, SCMInputChangeReason, InputValidationType, IInputValidation } from './scm'; import { ILogService } from 'vs/platform/log/common/log'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -69,19 +69,6 @@ class SCMInput implements ISCMInput { private readonly _onDidChangeVisibility = new Emitter(); readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; - private _actionButton: ISCMActionButtonDescriptor | undefined; - get actionButton(): ISCMActionButtonDescriptor | undefined { - return this._actionButton; - } - - set actionButton(actionButton: ISCMActionButtonDescriptor) { - this._actionButton = actionButton; - this._onDidChangeActionButton.fire(); - } - - private readonly _onDidChangeActionButton = new Emitter(); - readonly onDidChangeActionButton: Event = this._onDidChangeActionButton.event; - setFocus(): void { this._onDidChangeFocus.fire(); } diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 1dad72b865b..c9a116bdbfe 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -319,12 +319,13 @@ 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) { + if (options.enableEditorSymbolSearch && options.includeSymbols) { const editorSymbolPicks = this.getEditorSymbolPicks(query, disposables, token); if (editorSymbolPicks) { return editorSymbolPicks; @@ -344,7 +345,26 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider(); if (options.additionPicks) { - picks.push(...options.additionPicks); + for (const pick of options.additionPicks) { + if (pick.type === 'separator') { + picks.push(pick); + continue; + } + if (!query.original) { + pick.highlights = undefined; + picks.push(pick); + continue; + } + const { score, labelMatch, descriptionMatch } = scoreItemFuzzy(pick, query, true, quickPickItemScorerAccessor, this.pickState.scorerCache); + if (!score) { + continue; + } + pick.highlights = { + label: labelMatch, + description: descriptionMatch + }; + picks.push(pick); + } } if (this.pickState.isQuickNavigating) { if (picks.length > 0) { @@ -364,7 +384,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider options.filter?.(p)) : picks, // Slow picks: files and symbols additionalPicks: (async (): Promise> => { @@ -377,13 +397,16 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider options.filter?.(p)); + } if (token.isCancellationRequested) { return []; } return additionalPicks.length > 0 ? [ - { type: 'separator', label: this.configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") }, + { type: 'separator', label: configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") }, ...additionalPicks ] : []; })(), @@ -393,12 +416,12 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider, token: CancellationToken): Promise> { + private async getAdditionalPicks(query: IPreparedQuery, excludes: ResourceMap, includeSymbols: boolean, token: CancellationToken): Promise> { // Resolve file and symbol picks (if enabled) const [filePicks, symbolPicks] = await Promise.all([ this.getFilePicks(query, excludes, token), - this.getWorkspaceSymbolPicks(query, token) + this.getWorkspaceSymbolPicks(query, includeSymbols, token) ]); if (token.isCancellationRequested) { @@ -806,11 +829,10 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> { - const configuration = this.configuration; + private async getWorkspaceSymbolPicks(query: IPreparedQuery, includeSymbols: boolean, token: CancellationToken): Promise> { if ( !query.normalized || // we need a value for search for - !configuration.includeSymbols || // we need to enable symbols in search + !includeSymbols || // we need to enable symbols in search this.pickState.lastRange // a range is an indicator for just searching for files ) { return []; diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts index bd4676d881c..e848ced9bb3 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -33,6 +33,7 @@ import { category, getElementsToOperateOn, getSearchView, openSearchView } from import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { Schemas } from 'vs/base/common/network'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; //#region Interfaces @@ -317,7 +318,7 @@ async function searchWithFolderCommand(accessor: ServicesAccessor, isFromExplore let resources: URI[]; if (isFromExplorer) { - resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IExplorerService)); + resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IExplorerService)); } else { const searchView = getSearchView(accessor.get(IViewsService)); if (!searchView) { 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 82693d67bdd..ce61496ec51 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -563,10 +563,10 @@ suite('SnippetsService', function () { assert.strictEqual(completions.items.length, 1); }); - test('issue #61296: VS code freezes when editing CSS file with emoji', async function () { + test('issue #61296: VS code freezes when editing CSS fi`le with emoji', async function () { const languageConfigurationService = disposables.add(new TestLanguageConfigurationService()); disposables.add(languageConfigurationService.register('fooLang', { - wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g + wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w\-?]+%?|[@#!.])/g })); snippetService = new SimpleSnippetService([new Snippet( diff --git a/src/vs/workbench/contrib/speech/browser/speechAccessibilitySignal.ts b/src/vs/workbench/contrib/speech/browser/speechAccessibilitySignal.ts index 5df6a700592..7bc183df9ed 100644 --- a/src/vs/workbench/contrib/speech/browser/speechAccessibilitySignal.ts +++ b/src/vs/workbench/contrib/speech/browser/speechAccessibilitySignal.ts @@ -17,6 +17,7 @@ export class SpeechAccessibilitySignalContribution extends Disposable implements @ISpeechService private readonly _speechService: ISpeechService, ) { super(); + this._register(this._speechService.onDidStartSpeechToTextSession(() => this._accessibilitySignalService.playSignal(AccessibilitySignal.voiceRecordingStarted))); this._register(this._speechService.onDidEndSpeechToTextSession(() => this._accessibilitySignalService.playSignal(AccessibilitySignal.voiceRecordingStopped))); } diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index 25d5c0ce951..b0efa674be4 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -12,7 +12,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ILogService } from 'vs/platform/log/common/log'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { DeferredPromise } from 'vs/base/common/async'; -import { ISpeechService, ISpeechProvider, HasSpeechProvider, ISpeechToTextSession, SpeechToTextInProgress, IKeywordRecognitionSession, KeywordRecognitionStatus, SpeechToTextStatus, speechLanguageConfigToLanguage, SPEECH_LANGUAGE_CONFIG, ITextToSpeechSession, TextToSpeechInProgress, TextToSpeechStatus } from 'vs/workbench/contrib/speech/common/speechService'; +import { ISpeechService, ISpeechProvider, HasSpeechProvider, ISpeechToTextSession, SpeechToTextInProgress, KeywordRecognitionStatus, SpeechToTextStatus, speechLanguageConfigToLanguage, SPEECH_LANGUAGE_CONFIG, ITextToSpeechSession, TextToSpeechInProgress, TextToSpeechStatus } from 'vs/workbench/contrib/speech/common/speechService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -126,7 +126,7 @@ export class SpeechService extends Disposable implements ISpeechService { this._onDidChangeHasSpeechProvider.fire(); } - //#region Transcription + //#region Speech to Text private readonly _onDidStartSpeechToTextSession = this._register(new Emitter()); readonly onDidStartSpeechToTextSession = this._onDidStartSpeechToTextSession.event; @@ -134,8 +134,8 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly _onDidEndSpeechToTextSession = this._register(new Emitter()); readonly onDidEndSpeechToTextSession = this._onDidEndSpeechToTextSession.event; - private _activeSpeechToTextSession: ISpeechToTextSession | undefined = undefined; - get hasActiveSpeechToTextSession() { return !!this._activeSpeechToTextSession; } + private activeSpeechToTextSessions = 0; + get hasActiveSpeechToTextSession() { return this.activeSpeechToTextSessions > 0; } private readonly speechToTextInProgress = SpeechToTextInProgress.bindTo(this.contextKeyService); @@ -143,7 +143,7 @@ export class SpeechService extends Disposable implements ISpeechService { const provider = await this.getProvider(); const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); - const session = this._activeSpeechToTextSession = provider.createSpeechToTextSession(token, typeof language === 'string' ? { language } : undefined); + const session = provider.createSpeechToTextSession(token, typeof language === 'string' ? { language } : undefined); const sessionStart = Date.now(); let sessionRecognized = false; @@ -153,38 +153,38 @@ export class SpeechService extends Disposable implements ISpeechService { const disposables = new DisposableStore(); const onSessionStoppedOrCanceled = () => { - if (session === this._activeSpeechToTextSession) { - this._activeSpeechToTextSession = undefined; + this.activeSpeechToTextSessions--; + if (!this.hasActiveSpeechToTextSession) { this.speechToTextInProgress.reset(); - this._onDidEndSpeechToTextSession.fire(); - - type SpeechToTextSessionClassification = { - owner: 'bpasero'; - comment: 'An event that fires when a speech to text session is created'; - context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; - sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration of the session.' }; - sessionRecognized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech was recognized.' }; - sessionError: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech resulted in error.' }; - sessionContentLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Length of the recognized text.' }; - sessionLanguage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Configured language for the session.' }; - }; - type SpeechToTextSessionEvent = { - context: string; - sessionDuration: number; - sessionRecognized: boolean; - sessionError: boolean; - sessionContentLength: number; - sessionLanguage: string; - }; - this.telemetryService.publicLog2('speechToTextSession', { - context, - sessionDuration: Date.now() - sessionStart, - sessionRecognized, - sessionError, - sessionContentLength, - sessionLanguage: language - }); } + this._onDidEndSpeechToTextSession.fire(); + + type SpeechToTextSessionClassification = { + owner: 'bpasero'; + comment: 'An event that fires when a speech to text session is created'; + context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; + sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration of the session.' }; + sessionRecognized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech was recognized.' }; + sessionError: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech resulted in error.' }; + sessionContentLength: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Length of the recognized text.' }; + sessionLanguage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Configured language for the session.' }; + }; + type SpeechToTextSessionEvent = { + context: string; + sessionDuration: number; + sessionRecognized: boolean; + sessionError: boolean; + sessionContentLength: number; + sessionLanguage: string; + }; + this.telemetryService.publicLog2('speechToTextSession', { + context, + sessionDuration: Date.now() - sessionStart, + sessionRecognized, + sessionError, + sessionContentLength, + sessionLanguage: language + }); disposables.dispose(); }; @@ -197,10 +197,9 @@ export class SpeechService extends Disposable implements ISpeechService { disposables.add(session.onDidChange(e => { switch (e.status) { case SpeechToTextStatus.Started: - if (session === this._activeSpeechToTextSession) { - this.speechToTextInProgress.set(true); - this._onDidStartSpeechToTextSession.fire(); - } + this.activeSpeechToTextSessions++; + this.speechToTextInProgress.set(true); + this._onDidStartSpeechToTextSession.fire(); break; case SpeechToTextStatus.Recognizing: sessionRecognized = true; @@ -240,7 +239,7 @@ export class SpeechService extends Disposable implements ISpeechService { //#endregion - //#region Synthesizer + //#region Text to Speech private readonly _onDidStartTextToSpeechSession = this._register(new Emitter()); readonly onDidStartTextToSpeechSession = this._onDidStartTextToSpeechSession.event; @@ -248,64 +247,69 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly _onDidEndTextToSpeechSession = this._register(new Emitter()); readonly onDidEndTextToSpeechSession = this._onDidEndTextToSpeechSession.event; - private _activeTextToSpeechSession: ITextToSpeechSession | undefined = undefined; - get hasActiveTextToSpeechSession() { return !!this._activeTextToSpeechSession; } + private activeTextToSpeechSessions = 0; + get hasActiveTextToSpeechSession() { return this.activeTextToSpeechSessions > 0; } private readonly textToSpeechInProgress = TextToSpeechInProgress.bindTo(this.contextKeyService); async createTextToSpeechSession(token: CancellationToken, context: string = 'speech'): Promise { const provider = await this.getProvider(); - const session = this._activeTextToSpeechSession = provider.createTextToSpeechSession(token); + const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); + const session = provider.createTextToSpeechSession(token, typeof language === 'string' ? { language } : undefined); const sessionStart = Date.now(); let sessionError = false; const disposables = new DisposableStore(); - const onSessionStoppedOrCanceled = () => { - if (session === this._activeTextToSpeechSession) { - this._activeTextToSpeechSession = undefined; + const onSessionStoppedOrCanceled = (dispose: boolean) => { + this.activeTextToSpeechSessions--; + if (!this.hasActiveTextToSpeechSession) { this.textToSpeechInProgress.reset(); - this._onDidEndTextToSpeechSession.fire(); - - type TextToSpeechSessionClassification = { - owner: 'bpasero'; - comment: 'An event that fires when a text to speech session is created'; - context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; - sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration of the session.' }; - sessionError: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech resulted in error.' }; - }; - type TextToSpeechSessionEvent = { - context: string; - sessionDuration: number; - sessionError: boolean; - }; - this.telemetryService.publicLog2('textToSpeechSession', { - context, - sessionDuration: Date.now() - sessionStart, - sessionError - }); } + this._onDidEndTextToSpeechSession.fire(); - disposables.dispose(); + type TextToSpeechSessionClassification = { + owner: 'bpasero'; + comment: 'An event that fires when a text to speech session is created'; + context: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Context of the session.' }; + sessionDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration of the session.' }; + sessionError: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'If speech resulted in error.' }; + sessionLanguage: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Configured language for the session.' }; + }; + type TextToSpeechSessionEvent = { + context: string; + sessionDuration: number; + sessionError: boolean; + sessionLanguage: string; + }; + this.telemetryService.publicLog2('textToSpeechSession', { + context, + sessionDuration: Date.now() - sessionStart, + sessionError, + sessionLanguage: language + }); + + if (dispose) { + disposables.dispose(); + } }; - disposables.add(token.onCancellationRequested(() => onSessionStoppedOrCanceled())); + disposables.add(token.onCancellationRequested(() => onSessionStoppedOrCanceled(true))); if (token.isCancellationRequested) { - onSessionStoppedOrCanceled(); + onSessionStoppedOrCanceled(true); } disposables.add(session.onDidChange(e => { switch (e.status) { case TextToSpeechStatus.Started: - if (session === this._activeTextToSpeechSession) { - this.textToSpeechInProgress.set(true); - this._onDidStartTextToSpeechSession.fire(); - } + this.activeTextToSpeechSessions++; + this.textToSpeechInProgress.set(true); + this._onDidStartTextToSpeechSession.fire(); break; case TextToSpeechStatus.Stopped: - onSessionStoppedOrCanceled(); + onSessionStoppedOrCanceled(false); break; case TextToSpeechStatus.Error: this.logService.error(`Speech provider error in text to speech session: ${e.text}`); @@ -327,15 +331,12 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly _onDidEndKeywordRecognition = this._register(new Emitter()); readonly onDidEndKeywordRecognition = this._onDidEndKeywordRecognition.event; - private _activeKeywordRecognitionSession: IKeywordRecognitionSession | undefined = undefined; - get hasActiveKeywordRecognition() { return !!this._activeKeywordRecognitionSession; } + private activeKeywordRecognitionSessions = 0; + get hasActiveKeywordRecognition() { return this.activeKeywordRecognitionSessions > 0; } async recognizeKeyword(token: CancellationToken): Promise { const result = new DeferredPromise(); - // Send out extension activation to ensure providers can register - await this.extensionService.activateByEvent('onSpeech'); - const disposables = new DisposableStore(); disposables.add(token.onCancellationRequested(() => { disposables.dispose(); @@ -398,16 +399,15 @@ export class SpeechService extends Disposable implements ISpeechService { private async doRecognizeKeyword(token: CancellationToken): Promise { const provider = await this.getProvider(); - const session = this._activeKeywordRecognitionSession = provider.createKeywordRecognitionSession(token); + const session = provider.createKeywordRecognitionSession(token); + this.activeKeywordRecognitionSessions++; this._onDidStartKeywordRecognition.fire(); const disposables = new DisposableStore(); const onSessionStoppedOrCanceled = () => { - if (session === this._activeKeywordRecognitionSession) { - this._activeKeywordRecognitionSession = undefined; - this._onDidEndKeywordRecognition.fire(); - } + this.activeKeywordRecognitionSessions--; + this._onDidEndKeywordRecognition.fire(); disposables.dispose(); }; diff --git a/src/vs/workbench/contrib/speech/common/speechService.ts b/src/vs/workbench/contrib/speech/common/speechService.ts index 4bd76b641aa..0b99c46ac57 100644 --- a/src/vs/workbench/contrib/speech/common/speechService.ts +++ b/src/vs/workbench/contrib/speech/common/speechService.ts @@ -14,9 +14,9 @@ import { language } from 'vs/base/common/platform'; export const ISpeechService = createDecorator('speechService'); -export const HasSpeechProvider = new RawContextKey('hasSpeechProvider', false, { type: 'string', description: localize('hasSpeechProvider', "A speech provider is registered to the speech service.") }); -export const SpeechToTextInProgress = new RawContextKey('speechToTextInProgress', false, { type: 'string', description: localize('speechToTextInProgress', "A speech-to-text session is in progress.") }); -export const TextToSpeechInProgress = new RawContextKey('textToSpeechInProgress', false, { type: 'string', description: localize('textToSpeechInProgress', "A text-to-speech session is in progress.") }); +export const HasSpeechProvider = new RawContextKey('hasSpeechProvider', false, { type: 'boolean', description: localize('hasSpeechProvider', "A speech provider is registered to the speech service.") }); +export const SpeechToTextInProgress = new RawContextKey('speechToTextInProgress', false, { type: 'boolean', description: localize('speechToTextInProgress', "A speech-to-text session is in progress.") }); +export const TextToSpeechInProgress = new RawContextKey('textToSpeechInProgress', false, { type: 'boolean', description: localize('textToSpeechInProgress', "A text-to-speech session is in progress.") }); export interface ISpeechProviderMetadata { readonly extension: ExtensionIdentifier; @@ -54,7 +54,7 @@ export interface ITextToSpeechEvent { export interface ITextToSpeechSession { readonly onDidChange: Event; - synthesize(text: string): void; + synthesize(text: string): Promise; } export enum KeywordRecognitionStatus { @@ -76,11 +76,15 @@ export interface ISpeechToTextSessionOptions { readonly language?: string; } +export interface ITextToSpeechSessionOptions { + readonly language?: string; +} + export interface ISpeechProvider { readonly metadata: ISpeechProviderMetadata; createSpeechToTextSession(token: CancellationToken, options?: ISpeechToTextSessionOptions): ISpeechToTextSession; - createTextToSpeechSession(token: CancellationToken): ITextToSpeechSession; + createTextToSpeechSession(token: CancellationToken, options?: ITextToSpeechSessionOptions): ITextToSpeechSession; createKeywordRecognitionSession(token: CancellationToken): IKeywordRecognitionSession; } @@ -130,7 +134,13 @@ export interface ISpeechService { recognizeKeyword(token: CancellationToken): Promise; } -export const SPEECH_LANGUAGE_CONFIG = 'accessibility.voice.speechLanguage'; +export const enum AccessibilityVoiceSettingId { + SpeechTimeout = 'accessibility.voice.speechTimeout', + AutoSynthesize = 'accessibility.voice.autoSynthesize', + SpeechLanguage = 'accessibility.voice.speechLanguage', +} + +export const SPEECH_LANGUAGE_CONFIG = AccessibilityVoiceSettingId.SpeechLanguage; export const SPEECH_LANGUAGES = { ['da-DK']: { diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 1a02a115033..0e9b7879ba0 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -388,7 +388,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer // update tasks so an incomplete list isn't returned when getWorkspaceTasks is called this._workspaceTasksPromise = undefined; this._onDidRegisterSupportedExecutions.fire(); - if (custom && shell && process) { + if (Platform.isWeb || (custom && shell && process)) { this._onDidRegisterAllSupportedExecutions.fire(); } } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index c4d6e65f891..12ed79506ab 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -435,6 +435,9 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { public terminate(task: Task): Promise { const activeTerminal = this._activeTasks[task.getMapKey()]; + if (!activeTerminal) { + return Promise.resolve({ success: false, task: undefined }); + } const terminal = activeTerminal.terminal; if (!terminal) { return Promise.resolve({ success: false, task: undefined }); diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 511383f85c1..0de3b0a3839 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -1003,8 +1003,7 @@ namespace CommandConfiguration { runtime = Tasks.RuntimeType.fromString(config.type); } } - const isShellConfiguration = ShellConfiguration.is(config.isShellCommand); - if (Types.isBoolean(config.isShellCommand) || isShellConfiguration) { + if (Types.isBoolean(config.isShellCommand) || ShellConfiguration.is(config.isShellCommand)) { runtime = Tasks.RuntimeType.Shell; } else if (config.isShellCommand !== undefined) { runtime = !!config.isShellCommand ? Tasks.RuntimeType.Shell : Tasks.RuntimeType.Process; @@ -1034,8 +1033,8 @@ namespace CommandConfiguration { } if (config.options !== undefined) { result.options = CommandOptions.from(config.options, context); - if (result.options && result.options.shell === undefined && isShellConfiguration) { - result.options.shell = ShellConfiguration.from(config.isShellCommand as IShellConfiguration, context); + if (result.options && result.options.shell === undefined && ShellConfiguration.is(config.isShellCommand)) { + result.options.shell = ShellConfiguration.from(config.isShellCommand, context); if (context.engine !== Tasks.ExecutionEngine.Terminal) { context.taskLoadIssues.push(nls.localize('ConfigurationParser.noShell', 'Warning: shell configuration is only supported when executing tasks in the terminal.')); } @@ -1247,11 +1246,6 @@ export namespace ProblemMatcherConverter { } } -const partialSource: Partial = { - label: 'Workspace', - config: undefined -}; - export namespace GroupKind { export function from(this: void, external: string | IGroupKind | undefined): Tasks.TaskGroup | undefined { if (external === undefined) { @@ -1399,6 +1393,7 @@ namespace ConfigurationProperties { return _isEmpty(value, properties); } } +const label = 'Workspace'; namespace ConfiguringTask { @@ -1470,15 +1465,15 @@ namespace ConfiguringTask { let taskSource: Tasks.FileBasedTaskSource; switch (source) { case TaskConfigSource.User: { - taskSource = Object.assign({} as Tasks.IUserTaskSource, partialSource, { kind: Tasks.TaskSourceKind.User, config: configElement }); + taskSource = { kind: Tasks.TaskSourceKind.User, config: configElement, label }; break; } case TaskConfigSource.WorkspaceFile: { - taskSource = Object.assign({} as Tasks.WorkspaceFileTaskSource, partialSource, { kind: Tasks.TaskSourceKind.WorkspaceFile, config: configElement }); + taskSource = { kind: Tasks.TaskSourceKind.WorkspaceFile, config: configElement, label }; break; } default: { - taskSource = Object.assign({} as Tasks.IWorkspaceTaskSource, partialSource, { kind: Tasks.TaskSourceKind.Workspace, config: configElement }); + taskSource = { kind: Tasks.TaskSourceKind.Workspace, config: configElement, label }; break; } } @@ -1543,15 +1538,15 @@ namespace CustomTask { let taskSource: Tasks.FileBasedTaskSource; switch (source) { case TaskConfigSource.User: { - taskSource = Object.assign({} as Tasks.IUserTaskSource, partialSource, { kind: Tasks.TaskSourceKind.User, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder } }); + taskSource = { kind: Tasks.TaskSourceKind.User, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder }, label }; break; } case TaskConfigSource.WorkspaceFile: { - taskSource = Object.assign({} as Tasks.WorkspaceFileTaskSource, partialSource, { kind: Tasks.TaskSourceKind.WorkspaceFile, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder, workspace: context.workspace } }); + taskSource = { kind: Tasks.TaskSourceKind.WorkspaceFile, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder, workspace: context.workspace }, label }; break; } default: { - taskSource = Object.assign({} as Tasks.IWorkspaceTaskSource, partialSource, { kind: Tasks.TaskSourceKind.Workspace, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder } }); + taskSource = { kind: Tasks.TaskSourceKind.Workspace, config: { index, element: external, file: '.vscode/tasks.json', workspaceFolder: context.workspaceFolder }, label }; break; } } @@ -2110,7 +2105,7 @@ class ConfigurationParser { const name = Tasks.CommandString.value(globals.command.name); const task: Tasks.CustomTask = new Tasks.CustomTask( context.uuidMap.getUUID(name), - Object.assign({} as Tasks.IWorkspaceTaskSource, source, { config: { index: -1, element: fileConfig, workspaceFolder: context.workspaceFolder } }), + Object.assign({}, source, 'workspace', { config: { index: -1, element: fileConfig, workspaceFolder: context.workspaceFolder } }) satisfies Tasks.IWorkspaceTaskSource, name, Tasks.CUSTOMIZED_TASK_TYPE, { diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index 3ef60f6e047..3db39c2267d 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -15,7 +15,7 @@ import { ITaskSummary, ITaskTerminateResponse, ITaskSystemInfo } from 'vs/workbe import { IStringDictionary } from 'vs/base/common/collections'; import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -export { ITaskSummary, Task, ITaskTerminateResponse as TaskTerminateResponse }; +export type { ITaskSummary, Task, ITaskTerminateResponse as TaskTerminateResponse }; export const CustomExecutionSupportedContext = new RawContextKey('customExecutionSupported', false, nls.localize('tasks.customExecutionSupported', "Whether CustomExecution tasks are supported. Consider using in the when clause of a \'taskDefinition\' contribution.")); export const ShellExecutionSupportedContext = new RawContextKey('shellExecutionSupported', false, nls.localize('tasks.shellExecutionSupported', "Whether ShellExecution tasks are supported. Consider using in the when clause of a \'taskDefinition\' contribution.")); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index f4f0233b5cc..11401eecf70 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -60,10 +60,11 @@ import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/cap import { killTerminalIcon, newTerminalIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Iterable } from 'vs/base/common/iterator'; -import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown, accessibleViewOnLastLine } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibleViewCurrentProviderId, accessibleViewIsShown, accessibleViewOnLastLine } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { isKeyboardEvent, isMouseEvent, isPointerEvent } from 'vs/base/browser/dom'; import { editorGroupToColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { InstanceContext } from 'vs/workbench/contrib/terminal/browser/terminalContextMenu'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts index c6f6bdb61d4..a17b660a13c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts @@ -26,8 +26,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { showWithPinnedItems } from 'vs/platform/quickinput/browser/quickPickPin'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { AccessibleViewProviderId, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; export async function showRunRecentQuickPick( accessor: ServicesAccessor, diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index 11127e5226a..ed4925cadf6 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -230,6 +230,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { this.registerCommandDecoration(command); if (command.exitCode) { this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalCommandFailed); + } else { + this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalCommandSucceeded); } })); // Command invalidated diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index 98a4ef9657f..c2b8198873a 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -12,8 +12,6 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IAccessibleViewService, NavigationType } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityHelpAction, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerTerminalAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; @@ -35,6 +33,8 @@ import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/ac import { alert } from 'vs/base/browser/ui/aria/aria'; import { TerminalAccessibilitySettingId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminalAccessibilityConfiguration'; import { TerminalAccessibilityCommandId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminal.accessibility'; +import { IAccessibleViewService, AccessibleViewProviderId, NavigationType } from 'vs/platform/accessibility/browser/accessibleView'; +import { accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; // #region Terminal Contributions @@ -193,6 +193,8 @@ export class TerminalAccessibleViewContribution extends Disposable implements IT } if (command.exitCode) { this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalCommandFailed); + } else { + this._accessibilitySignalService.playSignal(AccessibilitySignal.terminalCommandSucceeded); } } diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts index ad967c57b5a..cd33345cc89 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts @@ -4,16 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { format } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ShellIntegrationStatus, TerminalSettingId, WindowsShellType } from 'vs/platform/terminal/common/terminal'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewContentProvider, IAccessibleViewOptions } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands'; import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -22,6 +17,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TerminalAccessibilitySettingId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminalAccessibilityConfiguration'; import { TerminalAccessibilityCommandId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminal.accessibility'; import { TerminalLinksCommandId } from 'vs/workbench/contrib/terminalContrib/links/common/terminal.links'; +import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { accessibleViewIsShown, accessibleViewCurrentProviderId, AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; export const enum ClassName { Active = 'active', @@ -50,37 +47,16 @@ export class TerminalAccessibilityHelpProvider extends Disposable implements IAc private readonly _instance: Pick, _xterm: Pick & { raw: Terminal }, @IInstantiationService _instantiationService: IInstantiationService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ICommandService private readonly _commandService: ICommandService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { super(); this._hasShellIntegration = _xterm.shellIntegration.status === ShellIntegrationStatus.VSCode; } - - private _descriptionForCommand(commandId: string, msg: string, noKbMsg: string): string { - if (commandId === TerminalCommandId.RunRecentCommand) { - const kb = this._keybindingService.lookupKeybindings(commandId); - // Run recent command has multiple keybindings. lookupKeybinding just returns the first one regardless of the when context. - // Thus, we have to check if accessibility mode is enabled to determine which keybinding to use. - const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized(); - if (isScreenReaderOptimized && kb[1]) { - format(msg, kb[1].getAriaLabel()); - } else if (kb[0]) { - format(msg, kb[0].getAriaLabel()); - } else { - return format(noKbMsg, commandId); - } - } - const kb = this._keybindingService.lookupKeybinding(commandId, this._contextKeyService)?.getAriaLabel(); - return !kb ? format(noKbMsg, commandId) : format(msg, kb); - } - provideContent(): string { const content = []; - content.push(this._descriptionForCommand(TerminalAccessibilityCommandId.FocusAccessibleBuffer, localize('focusAccessibleTerminalView', 'The Focus Accessible Terminal View ({0}) command enables screen readers to read terminal contents.'), localize('focusAccessibleTerminalViewNoKb', 'The Focus Terminal Accessible View command enables screen readers to read terminal contents and is currently not triggerable by a keybinding.'))); + content.push(localize('focusAccessibleTerminalView', 'The Focus Accessible Terminal View command enables screen readers to read terminal contents.', TerminalAccessibilityCommandId.FocusAccessibleBuffer)); content.push(localize('preserveCursor', 'Customize the behavior of the cursor when toggling between the terminal and accessible view with `terminal.integrated.accessibleViewPreserveCursorPosition.`')); if (!this._configurationService.getValue(TerminalAccessibilitySettingId.AccessibleViewFocusOnCommandExecution)) { content.push(localize('focusViewOnExecution', 'Enable `terminal.integrated.accessibleViewFocusOnCommandExecution` to automatically focus the terminal accessible view when a command is executed in the terminal.')); @@ -91,17 +67,17 @@ export class TerminalAccessibilityHelpProvider extends Disposable implements IAc if (this._hasShellIntegration) { const shellIntegrationCommandList = []; shellIntegrationCommandList.push(localize('shellIntegration', "The terminal has a feature called shell integration that offers an enhanced experience and provides useful commands for screen readers such as:")); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalAccessibilityCommandId.AccessibleBufferGoToNextCommand, localize('goToNextCommand', 'Go to Next Command ({0}) in the accessible view'), localize('goToNextCommandNoKb', 'Go to Next Command in the accessible view is currently not triggerable by a keybinding.'))); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalAccessibilityCommandId.AccessibleBufferGoToPreviousCommand, localize('goToPreviousCommand', 'Go to Previous Command ({0}) in the accessible view'), localize('goToPreviousCommandNoKb', 'Go to Previous Command in the accessible view is currently not triggerable by a keybinding.'))); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(AccessibilityCommandId.GoToSymbol, localize('goToSymbol', 'Go to Symbol ({0})'), localize('goToSymbolNoKb', 'Go to symbol is currently not triggerable by a keybinding.'))); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalCommandId.RunRecentCommand, localize('runRecentCommand', 'Run Recent Command ({0})'), localize('runRecentCommandNoKb', 'Run Recent Command is currently not triggerable by a keybinding.'))); - shellIntegrationCommandList.push('- ' + this._descriptionForCommand(TerminalCommandId.GoToRecentDirectory, localize('goToRecentDirectory', 'Go to Recent Directory ({0})'), localize('goToRecentDirectoryNoKb', 'Go to Recent Directory is currently not triggerable by a keybinding.'))); + shellIntegrationCommandList.push('- ' + localize('goToNextCommand', 'Go to Next Command in the accessible view', TerminalAccessibilityCommandId.AccessibleBufferGoToNextCommand)); + shellIntegrationCommandList.push('- ' + localize('goToPreviousCommand', 'Go to Previous Command in the accessible view', TerminalAccessibilityCommandId.AccessibleBufferGoToPreviousCommand)); + shellIntegrationCommandList.push('- ' + localize('goToSymbol', 'Go to Symbol', AccessibilityCommandId.GoToSymbol)); + shellIntegrationCommandList.push('- ' + localize('runRecentCommand', 'Run Recent Command', TerminalCommandId.RunRecentCommand)); + shellIntegrationCommandList.push('- ' + localize('goToRecentDirectory', 'Go to Recent Directory', TerminalCommandId.GoToRecentDirectory)); content.push(shellIntegrationCommandList.join('\n')); } else { - content.push(this._descriptionForCommand(TerminalCommandId.RunRecentCommand, localize('goToRecentDirectoryNoShellIntegration', 'The Go to Recent Directory command ({0}) enables screen readers to easily navigate to a directory that has been used in the terminal.'), localize('goToRecentDirectoryNoKbNoShellIntegration', 'The Go to Recent Directory command enables screen readers to easily navigate to a directory that has been used in the terminal and is currently not triggerable by a keybinding.'))); + content.push(localize('goToRecentDirectoryNoShellIntegration', 'The Go to Recent Directory command enables screen readers to easily navigate to a directory that has been used in the terminal.', TerminalCommandId.RunRecentCommand)); } - content.push(this._descriptionForCommand(TerminalLinksCommandId.OpenDetectedLink, localize('openDetectedLink', 'The Open Detected Link ({0}) command enables screen readers to easily open links found in the terminal.'), localize('openDetectedLinkNoKb', 'The Open Detected Link command enables screen readers to easily open links found in the terminal and is currently not triggerable by a keybinding.'))); - content.push(this._descriptionForCommand(TerminalCommandId.NewWithProfile, localize('newWithProfile', 'The Create New Terminal (With Profile) ({0}) command allows for easy terminal creation using a specific profile.'), localize('newWithProfileNoKb', 'The Create New Terminal (With Profile) command allows for easy terminal creation using a specific profile and is currently not triggerable by a keybinding.'))); + content.push(localize('openDetectedLink', 'The Open Detected Link command enables screen readers to easily open links found in the terminal.', TerminalLinksCommandId.OpenDetectedLink)); + content.push(localize('newWithProfile', 'The Create New Terminal (With Profile) command allows for easy terminal creation using a specific profile.', TerminalCommandId.NewWithProfile)); content.push(localize('focusAfterRun', 'Configure what gets focused after running selected text in the terminal with `{0}`.', TerminalSettingId.FocusAfterRun)); return content.join('\n\n'); } diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts index 05a1f753a81..329b73e2a31 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts @@ -6,12 +6,12 @@ import { Emitter } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/model'; +import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType, IAccessibleViewSymbol } from 'vs/platform/accessibility/browser/accessibleView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TerminalCapability, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewContentProvider, IAccessibleViewOptions, IAccessibleViewSymbol } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { BufferContentTracker } from 'vs/workbench/contrib/terminalContrib/accessibility/browser/bufferContentTracker'; import { TerminalAccessibilitySettingId } from 'vs/workbench/contrib/terminalContrib/accessibility/common/terminalAccessibilityConfiguration'; 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 14a3330d3f0..a2c974a0baa 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 @@ -3,11 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; -import { TerminalInlineChatAccessibleViewContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; +import { TerminalInlineChatAccessibleView } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; -import { TerminalChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; // #region Terminal Contributions @@ -17,13 +15,15 @@ registerTerminalContribution(TerminalChatController.ID, TerminalChatController, // #region Contributions -registerWorkbenchContribution2(TerminalInlineChatAccessibleViewContribution.ID, TerminalInlineChatAccessibleViewContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(TerminalChatAccessibilityHelpContribution.ID, TerminalChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); +AccessibleViewRegistry.register(new TerminalInlineChatAccessibleView()); +AccessibleViewRegistry.register(new TerminalChatAccessibilityHelp()); // #endregion // #region Actions 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'; // #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 60e40ed0dd2..dbead33ec9a 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 @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IDetachedTerminalInstance, ITerminalContribution, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import type { Terminal as RawXtermTerminal, IDecoration, ITerminalAddon } from '@xterm/xterm'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; @@ -21,7 +21,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ICommandService } from 'vs/platform/commands/common/commands'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInlineChatService, IInlineChatSessionProvider, InlineChatProviderChangeEvent } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { status } from 'vs/base/browser/ui/aria/aria'; import * as dom from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -29,6 +28,7 @@ import { TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; 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'; const $ = dom.$; @@ -38,7 +38,7 @@ export class InitialHintAddon extends Disposable implements ITerminalAddon { private readonly _disposables = this._register(new MutableDisposable()); constructor(private readonly _capabilities: ITerminalCapabilityStore, - private readonly _onDidChangeProviders: Event) { + private readonly _onDidChangeAgents: Event) { super(); } activate(terminal: RawXtermTerminal): void { @@ -58,8 +58,13 @@ export class InitialHintAddon extends Disposable implements ITerminalAddon { } })); } - - this._disposables.value?.add(Event.once(this._onDidChangeProviders)(() => this._onDidRequestCreateHint.fire())); + const agentListener = this._onDidChangeAgents((e) => { + if (e?.locations.includes(ChatAgentLocation.Terminal)) { + this._onDidRequestCreateHint.fire(); + agentListener.dispose(); + } + }); + this._disposables.value?.add(agentListener); } } @@ -80,27 +85,34 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm private readonly _instance: Pick | IDetachedTerminalInstance, processManager: ITerminalProcessManager | ITerminalProcessInfo | undefined, widgetManager: TerminalWidgetManager | undefined, - @IInlineChatService private readonly _inlineChatService: IInlineChatService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ITerminalService private readonly _terminalService: ITerminalService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, + @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { super(); } xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { - if (this._terminalService.instances.length !== 1) { + if (this._terminalGroupService.instances.length + this._terminalEditorService.instances.length !== 1) { // only show for the first terminal return; } this._xterm = xterm; - this._addon = this._register(this._instantiationService.createInstance(InitialHintAddon, this._instance.capabilities, this._inlineChatService.onDidChangeProviders)); + this._addon = this._register(this._instantiationService.createInstance(InitialHintAddon, this._instance.capabilities, this._chatAgentService.onDidChangeAgents)); this._xterm.raw.loadAddon(this._addon); this._register(this._addon.onDidRequestCreateHint(() => this._createHint())); } private _createHint(): void { const instance = this._instance instanceof TerminalInstance ? this._instance : undefined; - if (!instance || !this._xterm || this._hintWidget || instance?.capabilities.get(TerminalCapability.CommandDetection)?.hasInput) { + const commandDetectionCapability = instance?.capabilities.get(TerminalCapability.CommandDetection); + if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability?.hasInput || instance.reconnectionProperties) { + return; + } + + if (!this._configurationService.getValue(TerminalInitialHintSettingId.Enabled)) { return; } @@ -125,22 +137,37 @@ export class TerminalInitialHintContribution extends Disposable implements ITerm this._addon?.dispose(); })); + const inputModel = commandDetectionCapability.promptInputModel; + if (inputModel) { + this._register(inputModel.onDidChangeInput(() => { + if (inputModel.value) { + this._decoration?.dispose(); + this._addon?.dispose(); + } + })); + } + if (!this._decoration) { return; } this._register(this._decoration); this._register(this._decoration.onRender((e) => { - if (!this._hintWidget && this._xterm?.isFocused && this._terminalService.instances.length === 1) { - const chatProviders = [...this._inlineChatService.getAllProvider()]; - if (chatProviders?.length) { + if (!this._hintWidget && this._xterm?.isFocused && this._terminalGroupService.instances.length + this._terminalEditorService.instances.length === 1) { + const terminalAgents = this._chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Terminal)); + if (terminalAgents?.length) { const widget = this._register(this._instantiationService.createInstance(TerminalInitialHintWidget, instance)); this._addon?.dispose(); - this._hintWidget = widget.getDomNode(chatProviders); + this._hintWidget = widget.getDomNode(terminalAgents); if (!this._hintWidget) { return; } e.appendChild(this._hintWidget); e.classList.add('terminal-initial-hint'); + const font = this._xterm.getFont(); + if (font) { + e.style.fontFamily = font.fontFamily; + e.style.fontSize = font.fontSize + 'px'; + } } } if (this._hintWidget && this._xterm) { @@ -191,8 +218,8 @@ class TerminalInitialHintWidget extends Disposable { })); } - private _getHintInlineChat(providers: IInlineChatSessionProvider[]) { - const providerName = (providers.length === 1 ? providers[0].label : undefined) ?? this.productService.nameShort; + private _getHintInlineChat(agents: IChatAgent[]) { + const providerName = (agents.length === 1 ? agents[0].fullName : undefined) ?? this.productService.nameShort; let ariaLabel = `Ask ${providerName} something or start typing to dismiss.`; @@ -267,12 +294,12 @@ class TerminalInitialHintWidget extends Disposable { return { ariaLabel, hintHandler, hintElement }; } - getDomNode(providers: IInlineChatSessionProvider[]): HTMLElement { + getDomNode(agents: IChatAgent[]): HTMLElement { if (!this.domNode) { this.domNode = $('.terminal-initial-hint'); this.domNode!.style.paddingLeft = '4px'; - const { hintElement, ariaLabel } = this._getHintInlineChat(providers); + const { hintElement, ariaLabel } = this._getHintInlineChat(agents); this.domNode.append(hintElement); this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.TerminalChat)); @@ -291,4 +318,3 @@ class TerminalInitialHintWidget extends Disposable { super.dispose(); } } - diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index 584bdc753d8..f0bdac465ba 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -3,44 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; -export class TerminalChatAccessibilityHelpContribution extends Disposable { - static ID = 'terminalChatAccessiblityHelp'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(110, 'terminalChat', runAccessibilityHelpAction, TerminalChatContextKeys.focused)); +export class TerminalChatAccessibilityHelp implements IAccessibleViewImplentation { + readonly priority = 110; + readonly name = 'terminalChat'; + readonly when = TerminalChatContextKeys.focused; + readonly type = AccessibleViewType.Help; + getProvider(accessor: ServicesAccessor) { + const terminalService = accessor.get(ITerminalService); + + const instance = terminalService.activeInstance; + if (!instance) { + return; + } + + const helpText = getAccessibilityHelpText(accessor); + return { + id: AccessibleViewProviderId.TerminalChat, + verbositySettingKey: AccessibilityVerbositySettingId.TerminalChat, + provideContent: () => helpText, + onClose: () => TerminalChatController.get(instance)?.focus(), + options: { type: AccessibleViewType.Help } + }; } } -export async function runAccessibilityHelpAction(accessor: ServicesAccessor): Promise { - const accessibleViewService = accessor.get(IAccessibleViewService); - const terminalService = accessor.get(ITerminalService); - - const instance = terminalService.activeInstance; - if (!instance) { - return; - } - - const helpText = getAccessibilityHelpText(accessor); - accessibleViewService.show({ - id: AccessibleViewProviderId.TerminalChat, - verbositySettingKey: AccessibilityVerbositySettingId.TerminalChat, - provideContent: () => helpText, - onClose: () => TerminalChatController.get(instance)?.focus(), - options: { type: AccessibleViewType.Help } - }); -} - export function getAccessibilityHelpText(accessor: ServicesAccessor): string { const keybindingService = accessor.get(IKeybindingService); const content = []; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index f5bbe82de03..215f1fe6f05 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -3,36 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; +import { AccessibleViewProviderId, AccessibleViewType } from 'vs/platform/accessibility/browser/accessibleView'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; +import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -export class TerminalInlineChatAccessibleViewContribution extends Disposable { - static ID: 'terminalInlineChatAccessibleViewContribution'; - constructor() { - super(); - this._register(AccessibleViewAction.addImplementation(105, 'terminalInlineChat', accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const terminalService = accessor.get(ITerminalService); - const controller: TerminalChatController | undefined = terminalService.activeInstance?.getContribution(TerminalChatController.ID) ?? undefined; - if (!controller?.lastResponseContent) { - return false; - } - const responseContent = controller.lastResponseContent; - accessibleViewService.show({ - id: AccessibleViewProviderId.TerminalChat, - verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, - provideContent(): string { return responseContent; }, - onClose() { - controller.focus(); - }, - options: { type: AccessibleViewType.View } - }); - return true; - }, TerminalChatContextKeys.focused)); +export class TerminalInlineChatAccessibleView implements IAccessibleViewImplentation { + readonly priority = 105; + readonly name = 'terminalInlineChat'; + readonly type = AccessibleViewType.View; + readonly when = TerminalChatContextKeys.focused; + getProvider(accessor: ServicesAccessor) { + const terminalService = accessor.get(ITerminalService); + const controller: TerminalChatController | undefined = terminalService.activeInstance?.getContribution(TerminalChatController.ID) ?? undefined; + if (!controller?.lastResponseContent) { + return; + } + const responseContent = controller.lastResponseContent; + return { + id: AccessibleViewProviderId.TerminalChat, + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + options: { type: AccessibleViewType.View } + }; } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index ce8c9103c34..5734afb1b87 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -13,16 +13,17 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti 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, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatUserAction, IChatProgress, IChatService, ChatAgentVoteDirection } 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, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, ChatRequestModel, IChatRequestVariableData, IChatResponseModel, getHistoryEntriesFromModel } 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'; const enum Message { NONE = 0, @@ -77,13 +78,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr } readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); - readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); + get onDidHide() { return this.chatWidget?.onDidHide ?? Event.None; } private _terminalAgentName = 'terminal'; private _terminalAgentId: string | undefined; private readonly _model: MutableDisposable = this._register(new MutableDisposable()); + get scopedContextKeyService(): IContextKeyService { + return this._chatWidget?.value.inlineChatWidget.scopedContextKeyService ?? this._contextKeyService; + } + constructor( private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, @@ -136,7 +141,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (e.action.kind === 'bug') { this.acceptFeedback(undefined); } else if (e.action.kind === 'vote') { - this.acceptFeedback(e.action.direction === InteractiveSessionVoteDirection.Up); + this.acceptFeedback(e.action.direction === ChatAgentVoteDirection.Up); } } })); @@ -183,7 +188,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr action = { kind: 'bug' }; } else { this._sessionResponseVoteContextKey.set(helpful ? 'up' : 'down'); - action = { kind: 'vote', direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down }; + action = { kind: 'vote', direction: helpful ? ChatAgentVoteDirection.Up : ChatAgentVoteDirection.Down }; } // TODO:extract into helper method for (const request of model.getRequests()) { @@ -245,7 +250,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._requestActiveContextKey.reset(); } - async acceptInput(): Promise { + async acceptInput(): Promise { if (!this._model.value) { this._model.value = this._chatService.startSession(ChatAgentLocation.Terminal, CancellationToken.None); if (!this._model.value) { @@ -259,6 +264,16 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!this._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(); this._requestActiveContextKey.set(true); const cancellationToken = new CancellationTokenSource().token; @@ -273,6 +288,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } if (this._currentRequest) { model.acceptResponseProgress(this._currentRequest, progress); + completeResponseCreated(); } }; @@ -286,6 +302,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr variables: [] }; this._currentRequest = model.addRequest(request, requestVarData, 0); + completeResponseCreated(); const requestProps: IChatAgentRequest = { sessionId: model.sessionId, requestId: this._currentRequest!.id, @@ -297,7 +314,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr 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.updateFollowUps(undefined); this._chatWidget?.value.inlineChatWidget.updateProgress(true); this._chatWidget?.value.inlineChatWidget.updateInfo(GeneratingPhrase + '\u2026'); await task; @@ -310,6 +326,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.value.inlineChatWidget.updateToolbar(true); if (this._currentRequest) { model.completeResponse(this._currentRequest); + completeResponseCreated(); } this._lastResponseContent = responseContent; if (this._currentRequest) { @@ -327,6 +344,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); } } + return responseCreated.p; } updateInput(text: string, selectAll = true): void { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 841c856e64f..8b9aedb6bc9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -5,7 +5,7 @@ import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { Dimension, getActiveWindow, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { MicrotaskDelay } from 'vs/base/common/symbols'; import 'vs/css!./media/terminalChatWidget'; @@ -27,6 +27,9 @@ export class TerminalChatWidget extends Disposable { private readonly _container: HTMLElement; + private readonly _onDidHide = this._register(new Emitter()); + readonly onDidHide = this._onDidHide.event; + private readonly _inlineChatWidget: InlineChatWidget; public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } @@ -88,6 +91,11 @@ export class TerminalChatWidget extends Disposable { this._container.appendChild(this._inlineChatWidget.domNode); this._focusTracker = this._register(trackFocus(this._container)); + this._register(this._focusTracker.onDidBlur(() => { + if (!this.inlineChatWidget.responseContent) { + this.hide(); + } + })); this.hide(); } @@ -162,7 +170,6 @@ export class TerminalChatWidget extends Disposable { this._container.classList.add('hide'); this._reset(); this._inlineChatWidget.updateChatMessage(undefined); - this._inlineChatWidget.updateFollowUps(undefined); this._inlineChatWidget.updateProgress(false); this._inlineChatWidget.updateToolbar(false); this._inlineChatWidget.reset(); @@ -171,6 +178,7 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.value = ''; this._instance.focus(); this._setTerminalOffset(undefined); + this._onDidHide.fire(); } private _setTerminalOffset(offset: number | undefined) { if (offset === undefined || this._container.classList.contains('hide')) { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts index 3cd6c48a21e..d3a1a8a0b11 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts @@ -10,13 +10,10 @@ import { workbenchInstantiationService } from 'vs/workbench/test/browser/workben import { NullLogService } from 'vs/platform/log/common/log'; import { InitialHintAddon } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution'; import { getActiveDocument } from 'vs/base/browser/dom'; -import { IInlineChatSession, IInlineChatSessionProvider, InlineChatProviderChangeEvent } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { Emitter } from 'vs/base/common/event'; import { strictEqual } from 'assert'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ITextModel } from 'vs/editor/common/model'; -import { ISelection } from 'vs/editor/common/core/selection'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { ChatAgentLocation, IChatAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; // Test TerminalInitialHintAddon @@ -25,13 +22,35 @@ suite('Terminal Initial Hint Addon', () => { let eventCount = 0; let xterm: Terminal; let initialHintAddon: InitialHintAddon; - const _onDidChangeProviders: Emitter = new Emitter(); - const onDidChangeProviders = _onDidChangeProviders.event; + const _onDidChangeAgents: Emitter = new Emitter(); + const onDidChangeAgents = _onDidChangeAgents.event; + const agent: IChatAgent = { + id: 'termminal', + name: 'terminal', + extensionId: new ExtensionIdentifier('test'), + extensionPublisherId: 'test', + extensionDisplayName: 'test', + metadata: {}, + slashCommands: [{ name: 'test', description: 'test' }], + locations: [ChatAgentLocation.fromRaw('terminal')], + invoke: async () => { return {}; } + }; + const editorAgent: IChatAgent = { + id: 'editor', + name: 'editor', + extensionId: new ExtensionIdentifier('test-editor'), + extensionPublisherId: 'test-editor', + extensionDisplayName: 'test-editor', + metadata: {}, + slashCommands: [{ name: 'test', description: 'test' }], + locations: [ChatAgentLocation.fromRaw('editor')], + invoke: async () => { return {}; } + }; setup(() => { const instantiationService = workbenchInstantiationService({}, store); xterm = store.add(new Terminal()); const shellIntegrationAddon = store.add(new ShellIntegrationAddon('', true, undefined, new NullLogService)); - initialHintAddon = store.add(instantiationService.createInstance(InitialHintAddon, shellIntegrationAddon.capabilities, onDidChangeProviders)); + initialHintAddon = store.add(instantiationService.createInstance(InitialHintAddon, shellIntegrationAddon.capabilities, onDidChangeAgents)); store.add(initialHintAddon.onDidRequestCreateHint(() => eventCount++)); const testContainer = document.createElement('div'); getActiveDocument().body.append(testContainer); @@ -47,36 +66,32 @@ suite('Terminal Initial Hint Addon', () => { xterm.focus(); strictEqual(eventCount, 0); }); - test('hint is shown when there is a chat provider', () => { + test('hint is not shown when there is just an editor agent', () => { eventCount = 0; - const provider: IInlineChatSessionProvider = { - extensionId: new ExtensionIdentifier('test'), - label: 'blahblah', - prepareInlineChatSession(model: ITextModel, range: ISelection, token: CancellationToken): Promise { - throw new Error('Method not implemented.'); - }, - provideResponse() { - throw new Error('Method not implemented.'); - } - }; - _onDidChangeProviders.fire({ added: provider }); + _onDidChangeAgents.fire(editorAgent); xterm.focus(); + strictEqual(eventCount, 0); + }); + test('hint is shown when there is a terminal chat agent', () => { + eventCount = 0; + _onDidChangeAgents.fire(editorAgent); + xterm.focus(); + strictEqual(eventCount, 0); + _onDidChangeAgents.fire(agent); + strictEqual(eventCount, 1); + }); + test('hint is not shown again when another terminal chat agent is added if it has already shown', () => { + eventCount = 0; + _onDidChangeAgents.fire(agent); + xterm.focus(); + strictEqual(eventCount, 1); + _onDidChangeAgents.fire(agent); strictEqual(eventCount, 1); }); }); suite('Input', () => { test('hint is not shown when there has been input', () => { - const provider: IInlineChatSessionProvider = { - extensionId: new ExtensionIdentifier('test'), - label: 'blahblah', - prepareInlineChatSession(model: ITextModel, range: ISelection, token: CancellationToken): Promise { - throw new Error('Method not implemented.'); - }, - provideResponse() { - throw new Error('Method not implemented.'); - } - }; - _onDidChangeProviders.fire({ added: provider }); + _onDidChangeAgents.fire(agent); xterm.writeln('data'); setTimeout(() => { xterm.focus(); @@ -85,5 +100,3 @@ suite('Terminal Initial Hint Addon', () => { }); }); }); - - diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts index 89d73f49f25..c6064901930 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution.ts @@ -11,7 +11,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { AccessibleViewProviderId, accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { accessibleViewCurrentProviderId, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveInstanceAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; @@ -26,6 +26,7 @@ import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminalContrib/link import { TerminalLinkResolver } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkResolver'; import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { TerminalLinksCommandId } from 'vs/workbench/contrib/terminalContrib/links/common/terminal.links'; +import { AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView'; // #region Services diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts index 80a6e670c4a..0eee6b948a9 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts @@ -11,8 +11,6 @@ import { IDetectedLinks } from 'vs/workbench/contrib/terminalContrib/links/brows import { TerminalLinkQuickPickEvent, type IDetachedTerminalInstance, type ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import type { ILink } from '@xterm/xterm'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import type { TerminalLink } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLink'; import { Sequencer, timeout } from 'vs/base/common/async'; import { PickerEditorState } from 'vs/workbench/browser/quickaccess'; @@ -21,6 +19,7 @@ import { TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminalContrib/li import { ILabelService } from 'vs/platform/label/common/label'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibleViewProviderId, IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView'; export class TerminalLinkQuickpick extends DisposableStore { diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index 8b310a5d71b..67cc55a485d 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -4,41 +4,57 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Action } from 'vs/base/common/actions'; import { mapFindFirst } from 'vs/base/common/arraysFind'; -import { assertNever } from 'vs/base/common/assert'; +import { assert, assertNever } from 'vs/base/common/assert'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { isUriComponents, URI } from 'vs/base/common/uri'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; 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 { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { IModelDecorationOptions, ITextModel, InjectedTextCursorStops, InjectedTextOptions } from 'vs/editor/common/model'; -import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/browser/hoverOperation'; +import { IModelDecorationOptions, InjectedTextCursorStops, InjectedTextOptions, ITextModel } from 'vs/editor/common/model'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +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 { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; -import { testingCoverageMissingBranch } from 'vs/workbench/contrib/testing/browser/icons'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; +import { testingCoverageMissingBranch, testingCoverageReport, testingFilterIcon, testingRerunIcon } from 'vs/workbench/contrib/testing/browser/icons'; +import { ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; +import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; +import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { FileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +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 Coverage'); +const TOGGLE_INLINE_COMMAND_TEXT = localize('testing.toggleInlineCoverage', 'Toggle Inline'); const TOGGLE_INLINE_COMMAND_ID = 'testing.toggleInlineCoverage'; const BRANCH_MISS_INDICATOR_CHARS = 4; @@ -49,7 +65,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri private loadingCancellation?: CancellationTokenSource; private readonly displayedStore = this._register(new DisposableStore()); private readonly hoveredStore = this._register(new DisposableStore()); - private readonly lineHoverWidget: Lazy; + private readonly summaryWidget: Lazy; private decorationIds = new Map this._register(instantiationService.createInstance(LineHoverWidget, this.editor))); + 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); @@ -82,8 +99,13 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri return; } - const file = report.getUri(model.uri); + 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; } @@ -100,6 +122,16 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } })); + const toolbarEnabled = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configurationService); + this._register(autorun(reader => { + const c = fileCoverage.read(reader); + if (c && toolbarEnabled.read(reader)) { + this.summaryWidget.value.setCoverage(c); + } else { + this.summaryWidget.rawValue?.setCoverage(undefined); + } + })); + this._register(autorun(reader => { const c = fileCoverage.read(reader); if (c) { @@ -114,8 +146,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri const model = editor.getModel(); if (e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS && model) { this.hoverLineNumber(editor.getModel()!, e.target.position.lineNumber); - } else if (this.lineHoverWidget.hasValue && this.lineHoverWidget.value.getDomNode().contains(e.target.element)) { - // don't dismiss the hover } else if (CodeCoverageDecorations.showInline.get() && e.target.type === MouseTargetType.CONTENT_TEXT && model) { this.hoverInlineDecoration(model, e.target.position); } else { @@ -184,7 +214,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri const todo = [{ line: lineNumber, dir: 0 }]; const toEnable = new Set(); - const inlineEnabled = CodeCoverageDecorations.showInline.get(); if (!CodeCoverageDecorations.showInline.get()) { for (let i = 0; i < todo.length && i < MAX_HOVERED_LINES; i++) { const { line, dir } = todo[i]; @@ -215,16 +244,11 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri }); } - if (toEnable.size || inlineEnabled) { - this.lineHoverWidget.value.startShowingAt(lineNumber); - } - this.hoveredStore.add(this.editor.onMouseLeave(() => { this.hoveredStore.clear(); })); this.hoveredStore.add(toDisposable(() => { - this.lineHoverWidget.value.hide(); this.hoveredSubject = undefined; model.changeDecorations(e => { @@ -499,27 +523,6 @@ function tidyLocation(location: Range | Position): Range { return location; } -class LineHoverComputer implements IHoverComputer { - public line = -1; - - constructor(@IKeybindingService private readonly keybindingService: IKeybindingService) { } - - /** @inheritdoc */ - public computeSync(): IMarkdownString[] { - const strs: IMarkdownString[] = []; - - const s = new MarkdownString().appendMarkdown(`[${TOGGLE_INLINE_COMMAND_TEXT}](command:${TOGGLE_INLINE_COMMAND_ID})`); - s.isTrusted = true; - const binding = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); - if (binding) { - s.appendText(` (${binding.getLabel()})`); - } - strs.push(s); - - return strs; - } -} - function wrapInBackticks(str: string) { return '`' + str.replace(/[\n\r`]/g, '') + '`'; } @@ -531,95 +534,193 @@ function wrapName(functionNameOrCode: string) { return wrapInBackticks(functionNameOrCode); } -class LineHoverWidget extends Disposable implements IOverlayWidget { - public static readonly ID = 'editor.contrib.testingCoverageLineHoverWidget'; +class CoverageToolbarWidget extends Disposable implements IOverlayWidget { + private current: FileCoverage | undefined; + private registered = false; + private isRunning = false; + private readonly showStore = this._register(new DisposableStore()); + private readonly actionBar: ActionBar; + private readonly _domNode = dom.h('div.coverage-summary-widget', [ + dom.h('div', [ + dom.h('span.bars@bars'), + dom.h('span.toolbar@toolbar'), + ]), + ]); - private readonly computer: LineHoverComputer; - private readonly hoverOperation: HoverOperation; - private readonly hover = this._register(new HoverWidget()); - private readonly renderDisposables = this._register(new DisposableStore()); - private readonly markdownRenderer: MarkdownRenderer; + private readonly bars: ManagedTestCoverageBars; - constructor(private readonly editor: ICodeEditor, @IInstantiationService instantiationService: IInstantiationService) { + constructor( + private readonly editor: ICodeEditor, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @ITestService private readonly testService: ITestService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @ICommandService private readonly commandService: ICommandService, + @IInstantiationService instaService: IInstantiationService, + ) { super(); - this.computer = instantiationService.createInstance(LineHoverComputer); - this.markdownRenderer = this._register(instantiationService.createInstance(MarkdownRenderer, { editor: this.editor })); - this.hoverOperation = this._register(new HoverOperation(this.editor, this.computer)); - this.hover.containerDomNode.classList.add('hidden'); - this.hoverOperation.onResult(result => { - if (result.value.length) { - this.render(result.value); - } else { - this.hide(); + + this.bars = this._register(instaService.createInstance(ManagedTestCoverageBars, { + compact: false, + overall: false, + container: this._domNode.bars, + })); + + this.actionBar = this._register(instaService.createInstance(ActionBar, this._domNode.toolbar, { + orientation: ActionsOrientation.HORIZONTAL, + actionViewItemProvider: (action, options) => { + const vm = new CodiconActionViewItem(undefined, action, options); + if (action instanceof ActionWithIcon) { + vm.themeIcon = action.icon; + } + return vm; } - }); - this.editor.addOverlayWidget(this); + })); + + + this._register(autorun(reader => { + CodeCoverageDecorations.showInline.read(reader); + this.setActions(); + })); + + this._register(dom.addStandardDisposableListener(this._domNode.root, dom.EventType.CONTEXT_MENU, e => { + this.contextMenuService.showContextMenu({ + menuId: MenuId.StickyScrollContext, + getAnchor: () => e, + }); + })); } /** @inheritdoc */ - getId(): string { - return LineHoverWidget.ID; + public getId(): string { + return 'coverage-summary-widget'; } /** @inheritdoc */ public getDomNode(): HTMLElement { - return this.hover.containerDomNode; + return this._domNode.root; } /** @inheritdoc */ public getPosition(): IOverlayWidgetPosition | null { - return null; + return { + preference: OverlayWidgetPositionPreference.TOP_CENTER, + stackOridinal: 9, + }; } - /** @inheritdoc */ - public override dispose(): void { - this.editor.removeOverlayWidget(this); - super.dispose(); + public setCoverage(coverage: FileCoverage | undefined) { + this.current = coverage; + this.bars.setCoverageInfo(coverage); + + if (!coverage) { + this.hide(); + } else { + this.setActions(); + this.show(); + } } - /** Shows the hover widget at the given line */ - public startShowingAt(lineNumber: number) { - this.hide(); - const textModel = this.editor.getModel(); - if (!textModel) { + private setActions() { + this.actionBar.clear(); + const coverage = this.current; + if (!coverage) { return; } - this.computer.line = lineNumber; - this.hoverOperation.start(HoverStartMode.Delayed); - } + const toggleAction = new ActionWithIcon( + 'toggleInline', + CodeCoverageDecorations.showInline.get() + ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') + : localize('testing.showInlineCoverage', 'Show Inline Coverage'), + testingCoverageReport, + undefined, + () => CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined), + ); - /** Hides the hover widget */ - public hide() { - this.hoverOperation.cancel(); - this.hover.containerDomNode.classList.add('hidden'); - } - - private render(elements: IMarkdownString[]) { - const { hover: h, editor: editor } = this; - const fragment = document.createDocumentFragment(); - - for (const msg of elements) { - const markdownHoverElement = dom.$('div.hover-row.markdown-hover'); - const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents')); - const renderedContents = this.renderDisposables.add(this.markdownRenderer.render(msg)); - hoverContentsElement.appendChild(renderedContents.element); - fragment.appendChild(markdownHoverElement); + const kb = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); + if (kb) { + toggleAction.tooltip = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; } - dom.clearNode(h.contentsDomNode); - h.contentsDomNode.appendChild(fragment); + this.actionBar.push(toggleAction); - h.containerDomNode.classList.remove('hidden'); - const editorLayout = editor.getLayoutInfo(); - const topForLineNumber = editor.getTopForLineNumber(this.computer.line); - const editorScrollTop = editor.getScrollTop(); - const lineHeight = editor.getOption(EditorOption.lineHeight); - const nodeHeight = h.containerDomNode.clientHeight; - const top = topForLineNumber - editorScrollTop - ((nodeHeight - lineHeight) / 2); - const left = editorLayout.lineNumbersLeft + editorLayout.lineNumbersWidth; - h.containerDomNode.style.left = `${left}px`; - h.containerDomNode.style.top = `${Math.max(Math.round(top), 0)}px`; + if (coverage.isForTest) { + const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.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), + )); + } else if (coverage.perTestData?.size) { + this.actionBar.push(new ActionWithIcon('perTestFilter', + localize('testing.coverageForTestAvailable', "{0} test(s) ran code in this file", coverage.perTestData.size), + testingFilterIcon, + undefined, + () => this.commandService.executeCommand(TestCommandId.CoverageFilterToTestInEditor, this.current), + )); + } + + this.actionBar.push(new ActionWithIcon( + 'rerun', + localize('testing.rerun', 'Rerun'), + testingRerunIcon, + !this.isRunning, + () => this.rerunTest() + )); + } + + private show() { + if (this.registered) { + return; + } + + this.registered = true; + let viewZoneId: string; + const ds = this.showStore; + + this.editor.addOverlayWidget(this); + this.editor.changeViewZones(accessor => { + viewZoneId = accessor.addZone({ // make space for the widget + afterLineNumber: 0, + afterColumn: 0, + domNode: document.createElement('div'), + heightInPx: 30, + ordinal: -1, // show before code lenses + }); + }); + + ds.add(toDisposable(() => { + this.registered = false; + this.editor.removeOverlayWidget(this); + this.editor.changeViewZones(accessor => { + accessor.removeZone(viewZoneId); + }); + })); + + ds.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent)) { + this.setCoverage(this.current); + } + })); + } + + private rerunTest() { + const current = this.current; + if (current) { + this.isRunning = true; + this.setActions(); + this.testService.runResolvedTests(current.fromResult.request).finally(() => { + this.isRunning = false; + this.setActions(); + }); + } + } + + private hide() { + this.showStore.clear(); } } @@ -627,13 +728,17 @@ registerAction2(class ToggleInlineCoverage extends Action2 { constructor() { super({ id: TOGGLE_INLINE_COMMAND_ID, - title: localize2('coverage.toggleInline', "Toggle Inline Coverage"), + title: localize2('coverage.toggleInline', "Show Inline Coverage"), category: Categories.Test, keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI), }, - precondition: TestingContextKeys.isTestCoverageOpen, + icon: testingCoverageReport, + menu: [ + { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, + { id: MenuId.EditorTitle, when: ContextKeyExpr.and(TestingContextKeys.isTestCoverageOpen, TestingContextKeys.coverageToolbarEnabled.notEqualsTo(true)), group: 'navigation' }, + ] }); } @@ -641,3 +746,139 @@ registerAction2(class ToggleInlineCoverage extends Action2 { CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); } }); + +registerAction2(class ToggleCoverageToolbar extends Action2 { + constructor() { + super({ + id: TestCommandId.CoverageToggleToolbar, + title: localize2('testing.toggleToolbarTitle', "Test Coverage Toolbar"), + metadata: { + description: localize2('testing.toggleToolbarDesc', 'Toggle the sticky coverage bar in the editor.') + }, + category: Categories.Test, + toggled: { + condition: TestingContextKeys.coverageToolbarEnabled, + }, + menu: [ + { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, + { id: MenuId.StickyScrollContext, when: TestingContextKeys.isTestCoverageOpen }, + { id: MenuId.EditorTitle, when: TestingContextKeys.isTestCoverageOpen, group: 'coverage@1' }, + ] + }); + } + + run(accessor: ServicesAccessor): void { + const config = accessor.get(IConfigurationService); + const value = getTestingConfiguration(config, TestingConfigKeys.CoverageToolbarEnabled); + config.updateValue(TestingConfigKeys.CoverageToolbarEnabled, !value); + } +}); + +registerAction2(class FilterCoverageToTestInEditor extends Action2 { + constructor() { + super({ + id: TestCommandId.CoverageFilterToTestInEditor, + title: localize2('testing.filterActionLabel', "Filter Coverage to Test"), + category: Categories.Test, + icon: Codicon.filter, + toggled: { + icon: Codicon.filterFilled, + condition: TestingContextKeys.isCoverageFilteredToTest, + }, + menu: [ + { id: MenuId.EditorTitle, when: ContextKeyExpr.and(TestingContextKeys.isTestCoverageOpen, TestingContextKeys.coverageToolbarEnabled.notEqualsTo(true)), group: 'navigation' }, + ] + }); + } + + run(accessor: ServicesAccessor, coverageOrUri?: FileCoverage | URI): void { + const testCoverageService = accessor.get(ITestCoverageService); + const quickInputService = accessor.get(IQuickInputService); + const activeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor(); + let coverage: FileCoverage | undefined; + if (coverageOrUri instanceof FileCoverage) { + coverage = coverageOrUri; + } else if (isUriComponents(coverageOrUri)) { + coverage = testCoverageService.selected.get()?.getUri(URI.from(coverageOrUri)); + } else { + const uri = activeEditor?.getModel()?.uri; + 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) { + return; + } + + const tests = [...options.values()]; + const commonPrefix = TestId.getLengthOfCommonPrefix(tests.length, i => tests[i].isForTest!.id); + const result = coverage.fromResult; + const previousSelection = testCoverageService.filterToTest.get(); + + type TItem = { label: string; description?: string; item: FileCoverage | undefined }; + + const items: QuickPickInput[] = [ + { label: coverUtils.labels.allTests, item: undefined }, + { type: 'separator' }, + ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), + ]; + + // These handle the behavior that reveals the start of coverage when the + // user picks from the quickpick. Scroll position is restored if the user + // exits without picking an item, or picks "all tets". + const scrollTop = activeEditor?.getScrollTop() || 0; + const revealScrollCts = new MutableDisposable(); + + quickInputService.pick(items, { + activeItem: items.find((item): item is TItem => 'item' in item && item.item === coverage), + placeHolder: coverUtils.labels.pickShowCoverage, + onDidFocus: (entry) => { + if (!entry.item) { + revealScrollCts.clear(); + activeEditor?.setScrollTop(scrollTop); + testCoverageService.filterToTest.set(undefined, undefined); + } else { + const cts = revealScrollCts.value = new CancellationTokenSource(); + entry.item.details(cts.token).then( + details => { + const first = details.find(d => d.type === DetailType.Statement); + if (!cts.token.isCancellationRequested && first) { + activeEditor?.revealLineNearTop(first.location instanceof Position ? first.location.lineNumber : first.location.startLineNumber); + } + }, + () => { /* ignored */ } + ); + testCoverageService.filterToTest.set(entry.item.isForTest!.id, undefined); + } + }, + }).then(selected => { + if (!selected) { + activeEditor?.setScrollTop(scrollTop); + } + + revealScrollCts.dispose(); + testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); + }); + } +}); + +class ActionWithIcon extends Action { + constructor(id: string, title: string, public readonly icon: ThemeIcon, enabled: boolean | undefined, run: () => void) { + super(id, title, undefined, enabled, run); + } +} + +class CodiconActionViewItem extends ActionViewItem { + + public themeIcon?: ThemeIcon; + + protected override updateLabel(): void { + if (this.options.label && this.label && this.themeIcon) { + dom.reset(this.label, renderIcon(this.themeIcon), this.action.label); + } + } +} diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils.ts new file mode 100644 index 00000000000..d6a02334270 --- /dev/null +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertNever } from 'vs/base/common/assert'; +import { clamp } from 'vs/base/common/numbers'; +import { localize } from 'vs/nls'; +import { chartsGreen, chartsRed, chartsYellow } from 'vs/platform/theme/common/colorRegistry'; +import { asCssVariableName } from 'vs/platform/theme/common/colorUtils'; +import { CoverageBarSource } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; +import { ITestingCoverageBarThresholds, TestingDisplayedCoveragePercent } from 'vs/workbench/contrib/testing/common/configuration'; +import { getTotalCoveragePercent } 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 { ICoverageCount } from 'vs/workbench/contrib/testing/common/testTypes'; + +export const percent = (cc: ICoverageCount) => clamp(cc.total === 0 ? 1 : cc.covered / cc.total, 0, 1); + +const colorThresholds = [ + { color: `var(${asCssVariableName(chartsRed)})`, key: 'red' }, + { color: `var(${asCssVariableName(chartsYellow)})`, key: 'yellow' }, + { color: `var(${asCssVariableName(chartsGreen)})`, key: 'green' }, +] as const; + +export const getCoverageColor = (pct: number, thresholds: ITestingCoverageBarThresholds) => { + let best = colorThresholds[0].color; // red + let distance = pct; + for (const { key, color } of colorThresholds) { + const t = thresholds[key] / 100; + if (t && pct >= t && pct - t < distance) { + best = color; + distance = pct - t; + } + } + return best; +}; + + +const epsilon = 10e-8; + +export const displayPercent = (value: number, precision = 2) => { + const display = (value * 100).toFixed(precision); + + // avoid showing 100% coverage if it just rounds up: + if (value < 1 - epsilon && display === '100') { + return `${100 - (10 ** -precision)}%`; + } + + return `${display}%`; +}; + +export const calculateDisplayedStat = (coverage: CoverageBarSource, method: TestingDisplayedCoveragePercent) => { + switch (method) { + case TestingDisplayedCoveragePercent.Statement: + return percent(coverage.statement); + case TestingDisplayedCoveragePercent.Minimum: { + let value = percent(coverage.statement); + if (coverage.branch) { value = Math.min(value, percent(coverage.branch)); } + if (coverage.declaration) { value = Math.min(value, percent(coverage.declaration)); } + return value; + } + case TestingDisplayedCoveragePercent.TotalCoverage: + return getTotalCoveragePercent(coverage.statement, coverage.branch, coverage.declaration); + default: + assertNever(method); + } +}; + +export function getLabelForItem(result: LiveTestResult, testId: TestId, commonPrefixLen: number) { + const parts: string[] = []; + for (const id of testId.idsFromRoot()) { + const item = result.getTestById(id.toString()); + if (!item) { + break; + } + + parts.push(item.label); + } + + return parts.slice(commonPrefixLen).join(' \u203a '); +} + +export namespace labels { + export const showingFilterFor = (label: string) => localize('testing.coverageForTest', "Showing \"{0}\"", label); + export const clickToChangeFiltering = localize('changePerTestFilter', 'Click to view coverage for a single test'); + export const percentCoverage = (percent: number, precision?: number) => localize('testing.percentCoverage', '{0} Coverage', displayPercent(percent, precision)); + export const allTests = localize('testing.allTests', 'All tests'); + export const pickShowCoverage = localize('testing.pickTest', 'Pick a test to show coverage for'); +} diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index bd571b4a2df..5fa30f14ebd 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -267,6 +267,52 @@ cursor: pointer; } +.testing-followup-action { + position: absolute; + top: 100%; + left: 22px; + right: 22px; + margin-top: -25px; + line-height: 25px; + overflow: hidden; + pointer-events: none; + background: linear-gradient(transparent, var(--vscode-peekViewEditor-background) 50%); + display: flex; + align-items: center; + gap: 14px; + + &.animated { + animation: fadeIn 150ms ease-out; + } + + > a { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + pointer-events: auto; + width: fit-content; + flex-shrink: 0; + + &, .codicon { + color: var(--vscode-textLink-foreground); + } + + &:hover { + color: var(--vscode-textLink-activeForeground); + } + + &[aria-disabled="true"] { + color: inherit; + cursor: default; + + .codicon { + color: inherit; + } + } + } +} + /** -- filter */ .monaco-action-bar.testing-filter-action-bar { flex-shrink: 0; @@ -363,6 +409,10 @@ /** -- coverage */ +.coverage-view-is-filtered > .pane-header > .actions { + display: block !important; +} + .test-coverage-list-item { display: flex; align-items: center; @@ -401,6 +451,84 @@ opacity: 0.7; } +.coverage-summary-widget { + color: var(--vscode-editor-foreground); + z-index: 1; + background: var(--vscode-editor-background); + left: 0; + width: 100%; + box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; + + > div { + display: flex; + align-items: center; + padding: 0 22px; + height: 25px; + } + + .btn { + position: relative; + margin: 0 4px; + padding: 0 4px; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + + .stat, .action-label { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + margin: 0 3px; + } + + .action-label { + display: flex; + align-items: center; + font-size: 13px; + padding: 0 4px; + + .codicon { + margin-right: 4px; + } + } + +} + +.test-coverage-tree-per-test-switcher { + display: flex; + background-color: var(--vscode-dropdown-background); + color: var(--vscode-dropdown-foreground); + border: 1px solid var(--vscode-dropdown-border); + + margin: 3px 0; + cursor: pointer; + margin-right: 22px; + line-height: 20px; + padding: 0 6px; + width: fit-content; + max-width: calc(100% - 44px); + + span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &::after { + content: ''; + content: var(--vscode-icon-chevron-right-content); + font-family: var(--vscode-icon-chevron-right-font-family); + font-size: 18px; + padding-left: 22px; + } +} + /** -- coverage in the explorer */ .explorer-item-with-test-coverage { @@ -463,65 +591,67 @@ outline-offset: -1px; } -.coverage-deco-inline.coverage-deco-hit { - background: var(--vscode-testing-coveredBackground); - outline: 1px solid var(--vscode-testing-coveredBorder); -} +.monaco-editor { + .coverage-deco-inline.coverage-deco-hit { + background: var(--vscode-testing-coveredBackground); + outline: 1px solid var(--vscode-testing-coveredBorder); + } -.coverage-deco-inline.coverage-deco-miss { - background: var(--vscode-testing-uncoveredBackground); - outline: 1px solid var(--vscode-testing-uncoveredBorder); -} + .coverage-deco-inline.coverage-deco-miss { + background: var(--vscode-testing-uncoveredBackground); + outline: 1px solid var(--vscode-testing-uncoveredBorder); + } -.hc-light .coverage-deco-inline.coverage-deco-hit, -.hc-black .coverage-deco-inline.coverage-deco-hit { - outline-style: dashed; -} + .hc-light .coverage-deco-inline.coverage-deco-hit, + .hc-black .coverage-deco-inline.coverage-deco-hit { + outline-style: dashed; + } -.coverage-deco-branch-miss-indicator { - height: 100%; - width: 4ch; - position: relative; - display: inline-block; - font: inherit !important; -} + .coverage-deco-branch-miss-indicator { + height: 100%; + width: 4ch; + position: relative; + display: inline-block; + font: inherit !important; + } -.coverage-deco-branch-miss-indicator::before { - position: absolute; - top: 50%; - left: 50%; - text-align: center; - transform: translate(-50%, -50%); - padding: calc(var(--vscode-testing-coverage-lineHeight) / 10); - border-radius: 2px; - font: normal normal normal calc(var(--vscode-testing-coverage-lineHeight) / 2)/1 codicon; - border: 1px solid; -} + .coverage-deco-branch-miss-indicator::before { + position: absolute; + top: 50%; + left: 50%; + text-align: center; + transform: translate(-50%, -50%); + padding: calc(var(--vscode-testing-coverage-lineHeight) / 10); + border-radius: 2px; + font: normal normal normal calc(var(--vscode-testing-coverage-lineHeight) / 2)/1 codicon; + border: 1px solid; + } -.coverage-deco-inline-count { - position: relative; - background: var(--vscode-testing-coverCountBadgeBackground); - color: var(--vscode-testing-coverCountBadgeForeground); - font-size: 0.7em; - margin: 0 0.7em 0 0.4em; - padding: 0.2em 0 0.2em 0.2em; - /* display: inline-block; */ - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; -} + .coverage-deco-inline-count { + position: relative; + background: var(--vscode-testing-coverCountBadgeBackground); + color: var(--vscode-testing-coverCountBadgeForeground); + font-size: 0.7em; + margin: 0 0.7em 0 0.4em; + padding: 0.2em 0 0.2em 0.2em; + /* display: inline-block; */ + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; -.coverage-deco-inline-count::after { - content: ''; - display: block; - position: absolute; - left: 100%; - top: 0; - bottom: 0; - width: 0.5em; - background-image: - linear-gradient(to bottom left, transparent 50%, var(--vscode-testing-coverCountBadgeBackground) 0), - linear-gradient(to bottom right, var(--vscode-testing-coverCountBadgeBackground) 50%, transparent 0); - background-size: 100% 50%; - background-repeat: no-repeat; - background-position: top, bottom; + &::after { + content: ''; + display: block; + position: absolute; + left: 100%; + top: 0; + bottom: 0; + width: 0.5em; + background-image: + linear-gradient(to bottom left, transparent 50%, var(--vscode-testing-coverCountBadgeBackground) 0), + linear-gradient(to bottom right, var(--vscode-testing-coverCountBadgeBackground) 50%, transparent 0); + background-size: 100% 50%; + background-repeat: no-repeat; + background-position: top, bottom; + } + } } diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts index 7a485f44be7..3e4cf9e7131 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageBars.ts @@ -6,11 +6,9 @@ import { h } from 'vs/base/browser/dom'; import type { IUpdatableHover, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; -import { assertNever } from 'vs/base/common/assert'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { clamp } from 'vs/base/common/numbers'; import { ITransaction, autorun, observableValue } from 'vs/base/common/observable'; import { isDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; @@ -18,12 +16,12 @@ import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHoverService } from 'vs/platform/hover/browser/hover'; import { Registry } from 'vs/platform/registry/common/platform'; -import { asCssVariableName, chartsGreen, chartsRed, chartsYellow } from 'vs/platform/theme/common/colorRegistry'; import { ExplorerExtensions, IExplorerFileContribution, IExplorerFileContributionRegistry } from 'vs/workbench/contrib/files/browser/explorerFileContrib'; -import { ITestingCoverageBarThresholds, TestingConfigKeys, TestingDisplayedCoveragePercent, getTestingConfiguration, observeTestingConfiguration } from 'vs/workbench/contrib/testing/common/configuration'; -import { AbstractFileCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; +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'; -import { ICoverageCount } from 'vs/workbench/contrib/testing/common/testTypes'; export interface TestCoverageBarsOptions { /** @@ -31,6 +29,10 @@ export interface TestCoverageBarsOptions { * overall bar is shown and more details are given in the hover. */ compact: boolean; + /** + * Whether the overall stat is shown, defaults to true. + */ + overall?: boolean; /** * Container in which is render the bars. */ @@ -120,19 +122,21 @@ 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)); - el.overall.textContent = displayPercent(overallStat, precision); + if (this.options.overall !== false) { + el.overall.textContent = coverUtils.displayPercent(overallStat, precision); + } else { + el.overall.style.display = 'none'; + } if ('tpcBar' in el) { // compact mode renderBar(el.tpcBar, overallStat, false, thresholds); } else { - renderBar(el.statement, percent(coverage.statement), coverage.statement.total === 0, thresholds); - renderBar(el.function, coverage.declaration && percent(coverage.declaration), coverage.declaration?.total === 0, thresholds); - renderBar(el.branch, coverage.branch && percent(coverage.branch), coverage.branch?.total === 0, thresholds); + renderBar(el.statement, coverUtils.percent(coverage.statement), coverage.statement.total === 0, thresholds); + renderBar(el.function, coverage.declaration && coverUtils.percent(coverage.declaration), coverage.declaration?.total === 0, thresholds); + renderBar(el.branch, coverage.branch && coverUtils.percent(coverage.branch), coverage.branch?.total === 0, thresholds); } } } -const percent = (cc: ICoverageCount) => clamp(cc.total === 0 ? 1 : cc.covered / cc.total, 0, 1); -const epsilon = 10e-8; const barWidth = 16; const renderBar = (bar: HTMLElement, pct: number | undefined, isZero: boolean, thresholds: ITestingCoverageBarThresholds) => { @@ -152,59 +156,14 @@ const renderBar = (bar: HTMLElement, pct: number | undefined, isZero: boolean, t return; } - let best = colorThresholds[0].color; // red - let distance = pct; - for (const { key, color } of colorThresholds) { - const t = thresholds[key] / 100; - if (t && pct >= t && pct - t < distance) { - best = color; - distance = pct - t; - } - } - - bar.style.color = best; + bar.style.color = coverUtils.getCoverageColor(pct, thresholds); bar.style.opacity = '1'; }; -const colorThresholds = [ - { color: `var(${asCssVariableName(chartsRed)})`, key: 'red' }, - { color: `var(${asCssVariableName(chartsYellow)})`, key: 'yellow' }, - { color: `var(${asCssVariableName(chartsGreen)})`, key: 'green' }, -] as const; - -const calculateDisplayedStat = (coverage: CoverageBarSource, method: TestingDisplayedCoveragePercent) => { - switch (method) { - case TestingDisplayedCoveragePercent.Statement: - return percent(coverage.statement); - case TestingDisplayedCoveragePercent.Minimum: { - let value = percent(coverage.statement); - if (coverage.branch) { value = Math.min(value, percent(coverage.branch)); } - if (coverage.declaration) { value = Math.min(value, percent(coverage.declaration)); } - return value; - } - case TestingDisplayedCoveragePercent.TotalCoverage: - return getTotalCoveragePercent(coverage.statement, coverage.branch, coverage.declaration); - default: - assertNever(method); - } - -}; - -const displayPercent = (value: number, precision = 2) => { - const display = (value * 100).toFixed(precision); - - // avoid showing 100% coverage if it just rounds up: - if (value < 1 - epsilon && display === '100') { - return `${100 - (10 ** -precision)}%`; - } - - return `${display}%`; -}; - const nf = new Intl.NumberFormat(); -const stmtCoverageText = (coverage: CoverageBarSource) => localize('statementCoverage', '{0}/{1} statements covered ({2})', nf.format(coverage.statement.covered), nf.format(coverage.statement.total), displayPercent(percent(coverage.statement))); -const fnCoverageText = (coverage: CoverageBarSource) => coverage.declaration && localize('functionCoverage', '{0}/{1} functions covered ({2})', nf.format(coverage.declaration.covered), nf.format(coverage.declaration.total), displayPercent(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), displayPercent(percent(coverage.branch))); +const stmtCoverageText = (coverage: CoverageBarSource) => localize('statementCoverage', '{0}/{1} statements covered ({2})', nf.format(coverage.statement.covered), nf.format(coverage.statement.total), coverUtils.displayPercent(coverUtils.percent(coverage.statement))); +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 str = [ diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index c4270ad9e2a..8e67eebd282 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -23,7 +23,8 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { localize, localize2 } from 'vs/nls'; -import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -35,18 +36,21 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { IViewPaneOptions, ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; import { testingStatesToIcons, testingWasCovered } from 'vs/workbench/contrib/testing/browser/icons'; import { CoverageBarSource, ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; import { TestCommandId, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { onObservableChange } from 'vs/workbench/contrib/testing/common/observableUtils'; -import { ComputedFileCoverage, FileCoverage, TestCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; +import { BypassedFileCoverage, ComputedFileCoverage, FileCoverage, TestCoverage, getTotalCoveragePercent } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { CoverageDetails, DetailType, ICoverageCount, IDeclarationCoverage, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -86,11 +90,18 @@ export class TestCoverageView extends ViewPane { const coverage = this.coverageService.selected.read(reader); if (coverage) { const t = (this.tree.value ??= this.instantiationService.createInstance(TestCoverageTree, container, labels, this.sortOrder)); - t.setInput(coverage); + t.setInput(coverage, this.coverageService.filterToTest.read(reader)); } else { this.tree.clear(); } })); + + this._register(autorun(reader => { + this.element.classList.toggle( + 'coverage-view-is-filtered', + !!this.coverageService.filterToTest.read(reader), + ); + })); } protected override layoutBody(height: number, width: number): void { @@ -298,11 +309,18 @@ class TestCoverageTree extends Disposable { })); } - public setInput(coverage: TestCoverage) { + public setInput(coverage: TestCoverage, showOnlyTest?: TestId) { this.inputDisposables.clear(); - const files = []; - for (let node of coverage.tree.nodes) { + let tree = coverage.tree; + + // Filter to only a test, generate a new tree with only those items selected + if (showOnlyTest) { + tree = coverage.filterTreeForTest(showOnlyTest); + } + + const files: TestCoverageFileNode[] = []; + for (let node of tree.nodes) { // when showing initial children, only show from the first file or tee while (!(node.value instanceof FileCoverage) && node.children?.size === 1) { node = Iterable.first(node.children.values())!; @@ -310,15 +328,15 @@ class TestCoverageTree extends Disposable { files.push(node); } - const toChild = (file: TestCoverageFileNode): ICompressedTreeElement => { - const isFile = !file.children?.size; + const toChild = (value: TestCoverageFileNode): ICompressedTreeElement => { + const isFile = !value.children?.size; return { - element: file, + element: value, incompressible: isFile, collapsed: isFile, // directories can be expanded, and items with function info can be expanded - collapsible: !isFile || !!file.value?.declaration?.total, - children: file.children && Iterable.map(file.children?.values(), toChild) + collapsible: !isFile || !!value.value?.declaration?.total, + children: value.children && Iterable.map(value.children?.values(), toChild) }; }; @@ -485,12 +503,17 @@ class FileCoverageRenderer implements ICompressibleTreeRenderer basenameOrAuthority((e as TestCoverageFileNode).value!.uri)) : basenameOrAuthority(file.uri); - templateData.elementsDisposables.add(autorun(reader => { - stat.value?.didChange.read(reader); - templateData.bars.setCoverageInfo(file); - })); + if (file instanceof BypassedFileCoverage) { + templateData.bars.setCoverageInfo(undefined); + } else { + templateData.elementsDisposables.add(autorun(reader => { + stat.value?.didChange.read(reader); + templateData.bars.setCoverageInfo(file); + })); + + templateData.bars.setCoverageInfo(file); + } - templateData.bars.setCoverageInfo(file); templateData.label.setResource({ resource: file.uri, name }, { fileKind: stat.children?.size ? FileKind.FOLDER : FileKind.FILE, matches: createMatches(filterData), @@ -590,6 +613,62 @@ class TestCoverageIdentityProvider implements IIdentityProvider tests[i]); + const result = coverage.result; + const previousSelection = coverageService.filterToTest.get(); + const previousSelectionStr = previousSelection?.toString(); + + type TItem = { label: string; testId?: TestId }; + + const items: QuickPickInput[] = [ + { label: coverUtils.labels.allTests, id: undefined }, + { type: 'separator' }, + ...tests.map(testId => ({ label: coverUtils.getLabelForItem(result, testId, commonPrefix), testId })), + ]; + + quickInputService.pick(items, { + activeItem: items.find((item): item is TItem => 'testId' in item && item.testId?.toString() === previousSelectionStr), + placeHolder: coverUtils.labels.pickShowCoverage, + onDidFocus: (entry) => { + coverageService.filterToTest.set(entry.testId, undefined); + }, + }).then(selected => { + coverageService.filterToTest.set(selected ? selected.testId : previousSelection, undefined); + }); + } +}); + registerAction2(class TestCoverageChangeSortingAction extends ViewAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 16b2f5f1e92..6f90138b102 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -92,7 +92,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { }); }), ].filter(r => !this.state.text.value.includes(r.label)), - } as SuggestResultsProvider, + } satisfies SuggestResultsProvider, resourceHandle: 'testing:filter', suggestOptions: { value: this.state.text.value, diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 2bd0e5f3243..a05b7d50fe0 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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'; @@ -16,6 +17,7 @@ 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 { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; @@ -67,6 +69,7 @@ import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listSe 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'; @@ -97,7 +100,7 @@ import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/common/te 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 { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/testing/common/testResultService'; -import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; +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 { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { IShowResultOptions, ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener'; @@ -769,11 +772,115 @@ 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; @@ -810,6 +917,7 @@ class TestResultsViewContent extends Disposable { const { historyVisible, showRevealLocationOnMessages } = this.options; const isInPeekView = this.editor !== undefined; const messageContainer = this.messageContainer = dom.append(containerElement, dom.$('.test-output-peek-message-container')); + this.followupWidget = this._register(this.instantiationService.createInstance(FollowupActionWidget, messageContainer)); this.contentProviders = [ this._register(this.instantiationService.createInstance(DiffContentProvider, this.editor, messageContainer)), this._register(this.instantiationService.createInstance(MarkdownTestMessagePeek, messageContainer)), @@ -883,7 +991,7 @@ class TestResultsViewContent extends Disposable { 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); }); diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index 1bbc290e9cd..ec9e50d67f0 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -23,6 +23,7 @@ export const enum TestingConfigKeys { CoveragePercent = 'testing.displayedCoveragePercent', ShowCoverageInExplorer = 'testing.showCoverageInExplorer', CoverageBarThresholds = 'testing.coverageBarThresholds', + CoverageToolbarEnabled = 'testing.coverageToolbarEnabled', } export const enum AutoOpenTesting { @@ -190,6 +191,11 @@ export const testingConfiguration: IConfigurationNode = { green: { type: 'number', minimum: 0, maximum: 100, default: 90 }, }, }, + [TestingConfigKeys.CoverageToolbarEnabled]: { + description: localize('testing.coverageToolbarEnabled', 'Controls whether the coverage toolbar is shown in the editor.'), + type: 'boolean', + default: false, // todo@connor4312: disabled by default until UI sync + }, } }; @@ -214,6 +220,7 @@ export interface ITestingConfiguration { [TestingConfigKeys.CoveragePercent]: TestingDisplayedCoveragePercent; [TestingConfigKeys.ShowCoverageInExplorer]: boolean; [TestingConfigKeys.CoverageBarThresholds]: ITestingCoverageBarThresholds; + [TestingConfigKeys.CoverageToolbarEnabled]: boolean; } export const getTestingConfiguration = (config: IConfigurationService, key: K) => config.getValue(key); diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 8f6bb09e32a..0c098753237 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -63,11 +63,14 @@ export const enum TestCommandId { ContinousRunUsingForTest = 'testing.continuousRunUsingForTest', CoverageAtCursor = 'testing.coverageAtCursor', CoverageByUri = 'testing.coverage.uri', - CoverageViewChangeSorting = 'testing.coverageViewChangeSorting', CoverageClear = 'testing.coverage.close', CoverageCurrentFile = 'testing.coverageCurrentFile', + CoverageFilterToTest = 'testing.coverageFilterToTest', + CoverageFilterToTestInEditor = 'testing.coverageFilterToTestInEditor', CoverageLastRun = 'testing.coverageLastRun', CoverageSelectedAction = 'testing.coverageSelected', + CoverageToggleToolbar = 'testing.coverageToggleToolbar', + CoverageViewChangeSorting = 'testing.coverageViewChangeSorting', DebugAction = 'testing.debug', DebugAllAction = 'testing.debugAll', DebugAtCursor = 'testing.debugAtCursor', @@ -81,8 +84,8 @@ export const enum TestCommandId { GetSelectedProfiles = 'testing.getSelectedProfiles', GoToTest = 'testing.editFocusedTest', HideTestAction = 'testing.hideTest', - OpenOutputPeek = 'testing.openOutputPeek', OpenCoverage = 'testing.openCoverage', + OpenOutputPeek = 'testing.openOutputPeek', RefreshTestsAction = 'testing.refreshTests', ReRunFailedTests = 'testing.reRunFailTests', ReRunLastRun = 'testing.reRunLastRun', diff --git a/src/vs/workbench/contrib/testing/common/testCoverage.ts b/src/vs/workbench/contrib/testing/common/testCoverage.ts index 10d4f23cea3..321434bd602 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverage.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverage.ts @@ -3,6 +3,7 @@ * 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'; @@ -10,6 +11,8 @@ import { ITransaction, observableSignal } from 'vs/base/common/observable'; import { IPrefixTreeNode, WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; 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'; export interface ICoverageAccessor { @@ -25,18 +28,21 @@ export class TestCoverage { private readonly fileCoverage = new ResourceMap(); public readonly didAddCoverage = observableSignal[]>(this); 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, private readonly uriIdentityService: IUriIdentityService, private readonly accessor: ICoverageAccessor, ) { } - public append(rawCoverage: IFileCoverage, tx: ITransaction | undefined) { - const coverage = new FileCoverage(rawCoverage, this.accessor); + public append(coverage: IFileCoverage, tx: ITransaction | undefined) { const previous = this.getComputedForUri(coverage.uri); + const result = this.result; const applyDelta = (kind: 'statement' | 'branch' | 'declaration', node: ComputedFileCoverage) => { if (!node[kind]) { if (coverage[kind]) { @@ -53,31 +59,87 @@ export class TestCoverage { // version. const canonical = [...this.treePathForUri(coverage.uri, /* canonical = */ true)]; const chain: IPrefixTreeNode[] = []; - this.tree.insert(this.treePathForUri(coverage.uri, /* canonical = */ false), coverage, node => { + 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) { - node.value = coverage; - } else if (!node.value) { - // clone because later intersertions can modify the counts: - const intermediate = deepClone(rawCoverage); - intermediate.id = String(incId++); - intermediate.uri = this.treePathToUri(canonical.slice(0, chain.length)); - node.value = new ComputedFileCoverage(intermediate); - } else { - applyDelta('statement', node.value); - applyDelta('branch', node.value); - applyDelta('declaration', node.value); - node.value.didChange.trigger(tx); + // 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) { + 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; + v.statement = coverage.statement; + v.branch = coverage.branch; + v.declaration = coverage.declaration; + } else { + const v = node.value = new FileCoverage(coverage, result, this.accessor); + this.fileCoverage.set(coverage.uri, v); + } + } else if (!isPerTestCoverage) { + // 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. + if (!node.value) { + // clone because later intersertions can modify the counts: + const intermediate = deepClone(coverage); + intermediate.id = String(incId++); + intermediate.uri = this.treePathToUri(canonical.slice(0, chain.length)); + node.value = new ComputedFileCoverage(intermediate, result); + } else { + applyDelta('statement', node.value); + applyDelta('branch', node.value); + applyDelta('declaration', node.value); + node.value.didChange.trigger(tx); + } } }); - this.fileCoverage.set(coverage.uri, coverage); - if (chain) { + if (chain && !isPerTestCoverage) { this.didAddCoverage.trigger(tx, chain); } } + /** + * Builds a new tree filtered to per-test coverage data for the given ID. + */ + public filterTreeForTest(testId: TestId) { + const tree = new WellDefinedPrefixTree(); + for (const node of this.tree.values()) { + if (node instanceof FileCoverage) { + const fileData = node.perTestData?.get(testId.toString()); + if (!fileData) { + continue; + } + + const canonical = [...this.treePathForUri(fileData.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); + } + }); + } + } + + return tree; + } + /** * Gets coverage information for all files. */ @@ -131,7 +193,7 @@ export const getTotalCoveragePercent = (statement: ICoverageCount, branch: ICove }; export abstract class AbstractFileCoverage { - public readonly id: string; + public id: string; public readonly uri: URI; public statement: ICoverageCount; public branch?: ICoverageCount; @@ -146,7 +208,7 @@ export abstract class AbstractFileCoverage { return getTotalCoveragePercent(this.statement, this.branch, this.declaration); } - constructor(coverage: IFileCoverage) { + constructor(coverage: IFileCoverage, public readonly fromResult: LiveTestResult) { this.id = coverage.id; this.uri = coverage.uri; this.statement = coverage.statement; @@ -161,6 +223,15 @@ export abstract class AbstractFileCoverage { */ export class ComputedFileCoverage extends AbstractFileCoverage { } +/** + * A virtual node that doesn't have any added coverage info. + */ +export class BypassedFileCoverage extends ComputedFileCoverage { + constructor(uri: URI, result: LiveTestResult) { + super({ id: String(incId++), uri, statement: { covered: 0, total: 0 } }, result); + } +} + export class FileCoverage extends AbstractFileCoverage { private _details?: Promise; private resolved?: boolean; @@ -170,8 +241,18 @@ export class FileCoverage extends AbstractFileCoverage { return this._details instanceof Array || this.resolved; } - constructor(coverage: IFileCoverage, private readonly accessor: ICoverageAccessor) { - super(coverage); + /** + * Per-test coverage data for this file, if available. + */ + public perTestData?: Map; + + /** + * If this is for a single test item, gets the test item. + */ + public isForTest?: { id: TestId; parent: FileCoverage }; + + constructor(coverage: IFileCoverage, fromResult: LiveTestResult, private readonly accessor: ICoverageAccessor) { + super(coverage, fromResult); } /** diff --git a/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index 0bf62937458..e1b62d541dc 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -5,11 +5,15 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, observableValue } from 'vs/base/common/observable'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { bindContextKey, observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; import { ITestRunTaskResults } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -26,6 +30,11 @@ export interface ITestCoverageService { */ readonly selected: IObservable; + /** + * Filter to per-test coverage from the given test ID. + */ + readonly filterToTest: ISettableObservable; + /** * Opens a test coverage report from a task, optionally focusing it in the editor. */ @@ -39,18 +48,43 @@ export interface ITestCoverageService { export class TestCoverageService extends Disposable implements ITestCoverageService { declare readonly _serviceBrand: undefined; - private readonly _isOpenKey: IContextKey; private readonly lastOpenCts = this._register(new MutableDisposable()); public readonly selected = observableValue('testCoverage', undefined); + public readonly filterToTest = observableValue('filterToTest', undefined); constructor( @IContextKeyService contextKeyService: IContextKeyService, @ITestResultService resultService: ITestResultService, + @IConfigurationService configService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, ) { super(); - this._isOpenKey = TestingContextKeys.isTestCoverageOpen.bindTo(contextKeyService); + + const toolbarConfig = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configService); + this._register(bindContextKey( + TestingContextKeys.coverageToolbarEnabled, + contextKeyService, + reader => toolbarConfig.read(reader), + )); + + this._register(bindContextKey( + TestingContextKeys.isTestCoverageOpen, + contextKeyService, + reader => !!this.selected.read(reader), + )); + + this._register(bindContextKey( + TestingContextKeys.hasPerTestCoverage, + contextKeyService, + reader => !!this.selected.read(reader)?.perTestCoverageIDs.size, + )); + + this._register(bindContextKey( + TestingContextKeys.isCoverageFilteredToTest, + contextKeyService, + reader => !!this.filterToTest.read(reader), + )); this._register(resultService.onResultsChanged(evt => { if ('completed' in evt) { @@ -78,8 +112,11 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ return; } - this.selected.set(coverage, undefined); - this._isOpenKey.set(true); + transaction(tx => { + // todo: may want to preserve this if coverage for that test in the new run? + this.filterToTest.set(undefined, tx); + this.selected.set(coverage, tx); + }); if (focus && !cts.token.isCancellationRequested) { this.viewsService.openView(Testing.CoverageViewId, true); @@ -88,7 +125,6 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ /** @inheritdoc */ public closeCoverage() { - this._isOpenKey.set(false); this.selected.set(undefined, undefined); } } diff --git a/src/vs/workbench/contrib/testing/common/testId.ts b/src/vs/workbench/contrib/testing/common/testId.ts index 98bd5faec9b..79fcec77b1d 100644 --- a/src/vs/workbench/contrib/testing/common/testId.ts +++ b/src/vs/workbench/contrib/testing/common/testId.ts @@ -128,6 +128,27 @@ export class TestId { return TestPosition.Disconnected; } + public static getLengthOfCommonPrefix(length: number, getId: (i: number) => TestId): number { + if (length === 0) { + return 0; + } + + let commonPrefix = 0; + while (commonPrefix < length - 1) { + for (let i = 1; i < length; i++) { + const a = getId(i - 1); + const b = getId(i); + if (a.path[commonPrefix] !== b.path[commonPrefix]) { + return commonPrefix; + } + } + + commonPrefix++; + } + + return commonPrefix; + } + constructor( public readonly path: readonly string[], private readonly viewEnd = path.length, diff --git a/src/vs/workbench/contrib/testing/common/testResult.ts b/src/vs/workbench/contrib/testing/common/testResult.ts index 2fdd923c900..16b78d058cb 100644 --- a/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/src/vs/workbench/contrib/testing/common/testResult.ts @@ -313,6 +313,11 @@ export class LiveTestResult extends Disposable implements ITestResult { return this.testById.values(); } + /** Gets an included test item by ID. */ + public getTestById(id: string) { + return this.testById.get(id)?.item; + } + private readonly computedStateAccessor: IComputedStateAccessor = { getOwnState: i => i.ownComputedState, getCurrentComputedState: i => i.computedState, diff --git a/src/vs/workbench/contrib/testing/common/testService.ts b/src/vs/workbench/contrib/testing/common/testService.ts index 7af1ee2c331..d3026db22eb 100644 --- a/src/vs/workbench/contrib/testing/common/testService.ts +++ b/src/vs/workbench/contrib/testing/common/testService.ts @@ -12,7 +12,7 @@ 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 } from 'vs/workbench/contrib/testing/common/testTypes'; +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'; @@ -31,6 +31,12 @@ export interface IMainThreadTestController { runTests(request: IStartControllerTests[], token: CancellationToken): Promise; } +export interface IMainThreadTestHostProxy { + provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise; + executeTestFollowup(id: number): Promise; + disposeTestFollowups(ids: number[]): void; +} + export interface IMainThreadTestCollection extends AbstractIncrementalTestCollection { onBusyProvidersChange: Event; @@ -213,14 +219,6 @@ export const testsUnderUri = async function* (testService: ITestService, ident: } }; -/** - * An instance of the RootProvider should be registered for each extension - * host. - */ -export interface ITestRootProvider { - // todo: nothing, yet -} - /** * A run request that expresses the intent of the request and allows the * test service to resolve the specifics of the group. @@ -236,6 +234,15 @@ export interface AmbiguousRunTestsRequest { continuous?: boolean; } +export interface ITestFollowup { + message: string; + execute(): Promise; +} + +export interface ITestFollowups extends IDisposable { + followups: ITestFollowup[]; +} + export interface ITestService { readonly _serviceBrand: undefined; /** @@ -269,6 +276,11 @@ export interface ITestService { */ readonly showInlineOutput: MutableObservableValue; + /** + * Registers an interface that represents an extension host.. + */ + registerExtHost(controller: IMainThreadTestHostProxy): IDisposable; + /** * Registers an interface that runs tests for the given provider ID. */ @@ -304,6 +316,11 @@ export interface ITestService { */ runResolvedTests(req: ResolvedTestRunRequest, token?: CancellationToken): Promise; + /** + * Provides followup actions for a test run. + */ + provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise; + /** * Ensures the test diff from the remote ext host is flushed and waits for * any "busy" tests to become idle before resolving. diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index 04692c5dc81..fd3ffcd0999 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -27,13 +27,14 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; -import { AmbiguousRunTestsRequest, IMainThreadTestController, ITestService } from 'vs/workbench/contrib/testing/common/testService'; -import { ResolvedTestRunRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes'; +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 { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class TestService extends Disposable implements ITestService { declare readonly _serviceBrand: undefined; private testControllers = new Map(); + private testExtHosts = new Set(); private readonly cancelExtensionTestRunEmitter = new Emitter<{ runId: string | undefined }>(); private readonly willProcessDiffEmitter = new Emitter(); @@ -264,6 +265,32 @@ export class TestService extends Disposable implements ITestService { } } + /** + * @inheritdoc + */ + public async provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise { + const reqs = await Promise.all([...this.testExtHosts].map(async ctrl => + ({ ctrl, followups: await ctrl.provideTestFollowups(req, token) }))); + + const followups: ITestFollowups = { + followups: reqs.flatMap(({ ctrl, followups }) => followups.map(f => ({ + message: f.title, + execute: () => ctrl.executeTestFollowup(f.id) + }))), + dispose: () => { + for (const { ctrl, followups } of reqs) { + ctrl.disposeTestFollowups(followups.map(f => f.id)); + } + } + }; + + if (token.isCancellationRequested) { + followups.dispose(); + } + + return followups; + } + /** * @inheritdoc */ @@ -325,6 +352,14 @@ export class TestService extends Disposable implements ITestService { this.isRefreshingTests.set(false); } + /** + * @inheritdoc + */ + registerExtHost(controller: IMainThreadTestHostProxy): IDisposable { + this.testExtHosts.add(controller); + return toDisposable(() => this.testExtHosts.delete(controller)); + } + /** * @inheritdoc */ diff --git a/src/vs/workbench/contrib/testing/common/testTypes.ts b/src/vs/workbench/contrib/testing/common/testTypes.ts index cd14796eb0a..75f7b371362 100644 --- a/src/vs/workbench/contrib/testing/common/testTypes.ts +++ b/src/vs/workbench/contrib/testing/common/testTypes.ts @@ -474,6 +474,20 @@ export const applyTestItemUpdate = (internal: InternalTestItem | ITestItemUpdate } }; +/** Request to an ext host to get followup messages for a test failure. */ +export interface TestMessageFollowupRequest { + resultId: string; + extId: string; + taskIndex: number; + messageIndex: number; +} + +/** Request to an ext host to get followup messages for a test failure. */ +export interface TestMessageFollowupResponse { + id: number; + title: string; +} + /** * Test result item used in the main thread. */ @@ -559,6 +573,7 @@ export namespace ICoverageCount { export interface IFileCoverage { id: string; uri: URI; + testId?: TestId; statement: ICoverageCount; branch?: ICoverageCount; declaration?: ICoverageCount; @@ -568,6 +583,7 @@ export namespace IFileCoverage { export interface Serialized { id: string; uri: UriComponents; + testId: string | undefined; statement: ICoverageCount; branch?: ICoverageCount; declaration?: ICoverageCount; @@ -578,6 +594,7 @@ export namespace IFileCoverage { statement: original.statement, branch: original.branch, declaration: original.declaration, + testId: original.testId?.toString(), uri: original.uri.toJSON(), }); @@ -586,8 +603,16 @@ export namespace IFileCoverage { statement: serialized.statement, branch: serialized.branch, declaration: serialized.declaration, + testId: serialized.testId ? TestId.fromString(serialized.testId) : undefined, uri: uriIdentity.asCanonicalUri(URI.revive(serialized.uri)), }); + + export const empty = (id: string, uri: URI): IFileCoverage => ({ + id, + uri, + testId: undefined, + statement: ICoverageCount.empty(), + }); } function serializeThingWithLocation(serialized: T): T & { location?: IRange | IPosition } { diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index ddef4fcdc15..96682ebf7c9 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -22,6 +22,9 @@ export namespace TestingContextKeys { export const isParentRunningContinuously = new RawContextKey('testing.isParentRunningContinuously', false, { type: 'boolean', description: localize('testing.isParentRunningContinuously', 'Indicates whether the parent of a test is continuously running, set in the menu context of test items') }); export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') }); export const isTestCoverageOpen = new RawContextKey('testing.isTestCoverageOpen', false, { type: 'boolean', description: localize('testing.isTestCoverageOpen', 'Indicates whether a test coverage report is open') }); + export const hasPerTestCoverage = new RawContextKey('testing.hasPerTestCoverage', false, { type: 'boolean', description: localize('testing.hasPerTestCoverage', 'Indicates whether per-test coverage is available') }); + export const isCoverageFilteredToTest = new RawContextKey('testing.isCoverageFilteredToTest', false, { type: 'boolean', description: localize('testing.isCoverageFilteredToTest', 'Indicates whether coverage has been filterd to a single test') }); + export const coverageToolbarEnabled = new RawContextKey('testing.coverageToolbarEnabled', true, { type: 'boolean', description: localize('testing.coverageToolbarEnabled', 'Indicates whether the coverage toolbar is enabled') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, diff --git a/src/vs/workbench/contrib/testing/common/testingStates.ts b/src/vs/workbench/contrib/testing/common/testingStates.ts index 98b246bda2f..bcb75ddd6d7 100644 --- a/src/vs/workbench/contrib/testing/common/testingStates.ts +++ b/src/vs/workbench/contrib/testing/common/testingStates.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { mapValues } from 'vs/base/common/objects'; import { TestResultState } from 'vs/workbench/contrib/testing/common/testTypes'; export type TreeStateNode = { statusNode: true; state: TestResultState; priority: number }; @@ -25,13 +26,10 @@ export const statePriority: { [K in TestResultState]: number } = { export const isFailedState = (s: TestResultState) => s === TestResultState.Errored || s === TestResultState.Failed; export const isStateWithResult = (s: TestResultState) => s === TestResultState.Errored || s === TestResultState.Failed || s === TestResultState.Passed; -export const stateNodes = Object.entries(statePriority).reduce( - (acc, [stateStr, priority]) => { - const state = Number(stateStr) as TestResultState; - acc[state] = { statusNode: true, state, priority }; - return acc; - }, {} as { [K in TestResultState]: TreeStateNode } -); +export const stateNodes: { [K in TestResultState]: TreeStateNode } = mapValues(statePriority, (priority, stateStr): TreeStateNode => { + const state = Number(stateStr) as TestResultState; + return { statusNode: true, state, priority }; +}); export const cmpPriority = (a: TestResultState, b: TestResultState) => statePriority[b] - statePriority[a]; diff --git a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts index e0923164c97..94b282e74b8 100644 --- a/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts +++ b/src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts @@ -46,7 +46,7 @@ class TestObjectTree extends ObjectTree { disposeElement: (_el, _index, { store }) => store.clear(), renderTemplate: container => ({ container, store: new DisposableStore() }), templateId: 'default' - } as ITreeRenderer + } satisfies ITreeRenderer ], { sorter: sorter ?? { diff --git a/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts new file mode 100644 index 00000000000..ea8bb1e5b6e --- /dev/null +++ b/src/vs/workbench/contrib/testing/test/common/testCoverage.test.ts @@ -0,0 +1,190 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/*--------------------------------------------------------------------------------------------- + * 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 { 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'; + +suite('TestCoverage', () => { + let sandbox: SinonSandbox; + let coverageAccessor: ICoverageAccessor; + let testCoverage: TestCoverage; + + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + sandbox = createSandbox(); + coverageAccessor = { + getCoverageDetails: sandbox.stub().resolves([]), + }; + testCoverage = new TestCoverage({} as LiveTestResult, 'taskId', { extUri: { ignorePathCasing: () => true } } as any, coverageAccessor); + }); + + teardown(() => { + sandbox.restore(); + }); + + function addTests() { + const raw1: IFileCoverage = { + id: '1', + uri: URI.file('/path/to/file'), + statement: { covered: 10, total: 20 }, + branch: { covered: 5, total: 10 }, + declaration: { covered: 2, total: 5 }, + }; + + testCoverage.append(raw1, undefined); + + const raw2: IFileCoverage = { + id: '1', + uri: URI.file('/path/to/file2'), + statement: { covered: 5, total: 10 }, + branch: { covered: 1, total: 5 }, + }; + + testCoverage.append(raw2, undefined); + + return { raw1, raw2 }; + } + + test('should look up file coverage', async () => { + const { raw1 } = addTests(); + + const fileCoverage = testCoverage.getUri(raw1.uri); + assert.equal(fileCoverage?.id, raw1.id); + assert.deepEqual(fileCoverage?.statement, raw1.statement); + assert.deepEqual(fileCoverage?.branch, raw1.branch); + assert.deepEqual(fileCoverage?.declaration, raw1.declaration); + + assert.strictEqual(testCoverage.getComputedForUri(raw1.uri), testCoverage.getUri(raw1.uri)); + assert.strictEqual(testCoverage.getComputedForUri(URI.file('/path/to/x')), undefined); + assert.strictEqual(testCoverage.getUri(URI.file('/path/to/x')), undefined); + }); + + test('should compute coverage for directories', async () => { + const { raw1 } = addTests(); + const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); + assert.deepEqual(dirCoverage?.statement, { covered: 15, total: 30 }); + assert.deepEqual(dirCoverage?.branch, { covered: 6, total: 15 }); + assert.deepEqual(dirCoverage?.declaration, raw1.declaration); + }); + + test('should incrementally diff updates to existing files', async () => { + addTests(); + + const raw3: IFileCoverage = { + id: '1', + 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); + assert.deepEqual(fileCoverage?.statement, raw3.statement); + assert.deepEqual(fileCoverage?.branch, raw3.branch); + assert.deepEqual(fileCoverage?.declaration, raw3.declaration); + + const dirCoverage = testCoverage.getComputedForUri(URI.file('/path/to')); + assert.deepEqual(dirCoverage?.statement, { covered: 17, total: 34 }); + assert.deepEqual(dirCoverage?.branch, { covered: 8, total: 15 }); + assert.deepEqual(dirCoverage?.declaration, raw3.declaration); + }); + + test('should emit changes', async () => { + const changes: string[][] = []; + ds.add(onObservableChange(testCoverage.didAddCoverage, value => + changes.push(value.map(v => v.value!.uri.toString())))); + + addTests(); + + assert.deepStrictEqual(changes, [ + [ + "file:///", + "file:///", + "file:///", + "file:///path", + "file:///path/to", + "file:///path/to/file", + ], + [ + "file:///", + "file:///", + "file:///", + "file:///path", + "file:///path/to", + "file:///path/to/file2", + ], + ]); + }); + + 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/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index 24293e2bde3..def265bcd48 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -815,18 +815,18 @@ registerAction2(class extends Action2 { }); const ThemesSubMenu = new MenuId('ThemesSubMenu'); -MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { +MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { title: localize('themes', "Themes"), submenu: ThemesSubMenu, group: '2_configuration', order: 7 -}); -MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { +} satisfies ISubmenuItem); +MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { title: localize({ key: 'miSelectTheme', comment: ['&& denotes a mnemonic'] }, "&&Theme"), submenu: ThemesSubMenu, group: '2_configuration', order: 7 -}); +} satisfies ISubmenuItem); MenuRegistry.appendMenuItem(ThemesSubMenu, { command: { diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 606c0ed8582..f152f086e9d 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -418,7 +418,7 @@ export class TimelinePane extends ViewPane { } private onTimelineChanged(e: TimelineChangeEvent) { - if (e?.uri === undefined || this.uriIdentityService.extUri.isEqual(e.uri, this.uri)) { + if (e?.uri === undefined || this.uriIdentityService.extUri.isEqual(URI.revive(e.uri), this.uri)) { const timeline = this.timelinesBySource.get(e.id); if (timeline === undefined) { return; diff --git a/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts b/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts new file mode 100644 index 00000000000..61c252892ae --- /dev/null +++ b/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { isURLDomainTrusted, ITrustedDomainService } from 'vs/workbench/contrib/url/browser/trustedDomainService'; + +export class MockTrustedDomainService implements ITrustedDomainService { + _serviceBrand: undefined; + + constructor(private readonly _trustedDomains: string[] = []) { + } + + isValid(resource: URI): boolean { + return isURLDomainTrusted(resource, this._trustedDomains); + } +} diff --git a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css new file mode 100644 index 00000000000..0233dfd5309 --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.profiles-editor { + height: 100%; + overflow: hidden; + max-width: 1000px; + margin: 20px auto 0px auto; +} + +.profiles-editor .sidebar-view, +.profiles-editor .contents-view { + height: 100%; +} + +.profiles-editor .contents-container, +.profiles-editor .sidebar-container { + padding: 0px 20px; + height: 100%; +} + +.profiles-editor .sidebar-container .new-profile-button { + display: flex; + align-items: center; +} + +.profiles-editor .sidebar-container .new-profile-button > .monaco-button-dropdown { + flex-grow: 1; +} + +.profiles-editor .monaco-button-dropdown > .monaco-dropdown-button { + display: flex; + align-items: center; + padding: 0 4px; +} + +.profiles-editor .sidebar-container .profiles-tree { + margin-top: 10px; +} + +.profiles-editor .sidebar-container .profiles-tree .profile-tree-item { + display: flex; + align-items: center; +} + +.profiles-editor .sidebar-container .profiles-tree .profile-tree-item > * { + margin-right: 5px; +} + +.profiles-editor .sidebar-container .profiles-tree .profile-tree-item > .profile-tree-item-description { + margin-left: 2px; + display: flex; + align-items: center; + font-size: 0.9em; + opacity: 0.7; +} + +.profiles-editor .hide { + display: none !important; +} + +.profiles-editor .contents-container .profile-header { + display: flex; + height: 34px; + align-items: center; +} + +.profiles-editor .contents-container .profile-header .profile-title { + font-size: x-large; + font-weight: bold; + flex: 1; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container { + display: flex; + height: 28px; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container { + gap: 4px; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container .actions-container .codicon { + font-size: 18px; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container { + margin-right: 5px; + min-width: 120px; +} + +.profiles-editor .contents-container .profile-header .profile-actions-container .profile-button-container .monaco-button { + padding-left: 10px; + padding-right: 10px; +} + +.profiles-editor .contents-container .profile-body { + margin-top: 20px; +} + +.profiles-editor .contents-container .profile-name-container { + margin: 0px 0px 20px 15px; + display: flex; + width: 330px; + align-items: center; +} + +.profiles-editor .contents-container .profile-name-container .codicon { + cursor: pointer; + font-size: 20px; + padding: 2px; +} + +.profiles-editor .contents-container .profile-name-container .monaco-inputbox { + flex: 1; + margin-left: 10px; +} + +.profiles-editor .contents-container .profile-select-container { + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.profiles-editor .contents-container .profile-select-container > .monaco-select-box { + cursor: pointer; + line-height: 17px; + padding: 2px 23px 2px 8px; + border-radius: 2px; +} + +.profiles-editor .contents-container .profile-copy-from-container { + display: flex; + align-items: center; + margin: 0px 0px 20px 20px; +} + +.profiles-editor .contents-container .profile-copy-from-container > .profile-copy-from-label { + margin-right: 10px; + display: inline-flex; + align-items: center; +} + +.profiles-editor .contents-container .profile-copy-from-container > .profile-select-container { + width: 250px; +} + +.profiles-editor .contents-container .profile-contents-container { + margin: 0px 0px 10px 20px; + font-size: medium; +} + +.profiles-editor .contents-container .profile-tree-item-container { + display: flex; + align-items: center; +} + +.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-resource-type-label-container { + width: 150px; +} + +.profiles-editor .contents-container .profile-tree-item-container.new-profile-resource-type-container > .profile-select-container { + width: 170px; +} + +.profiles-editor .contents-container .profile-tree-item-container .profile-resource-type-description { + margin-left: 10px; + font-size: 0.9em; + opacity: 0.7; +} diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 0c5405c87b0..5ca000e81a7 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -7,7 +7,7 @@ import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/ import { isWeb } from 'vs/base/common/platform'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize, localize2 } from 'vs/nls'; -import { Action2, IMenuService, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, IMenuService, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -23,6 +23,13 @@ import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspac import { getErrorMessage } from 'vs/base/common/errors'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; +import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; +import { UserDataProfilesEditor, UserDataProfilesEditorInput, UserDataProfilesEditorInputSerializer } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor'; +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'; type IProfileTemplateQuickPickItem = IQuickPickItem & IProfileTemplateInfo; @@ -62,6 +69,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.registerEditor(); this.registerActions(); if (isWeb) { @@ -71,8 +79,23 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.reportWorkspaceProfileInfo(); } + private registerEditor(): void { + Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create( + UserDataProfilesEditor, + UserDataProfilesEditor.ID, + localize('userdataprofilesEditor', "Profiles Editor") + ), + [ + new SyncDescriptor(UserDataProfilesEditorInput) + ] + ); + Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(UserDataProfilesEditorInput.ID, UserDataProfilesEditorInputSerializer); + } + private registerActions(): void { this.registerProfileSubMenu(); + this._register(this.registerManageProfilesAction()); this._register(this.registerSwitchProfileAction()); this.registerProfilesActions(); @@ -90,9 +113,9 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements private registerProfileSubMenu(): void { const getProfilesTitle = () => { - return localize('profiles', "Profiles ({0})", this.userDataProfileService.currentProfile.name); + return localize('profiles', "Profile ({0})", this.userDataProfileService.currentProfile.name); }; - MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { get title() { return getProfilesTitle(); }, @@ -100,7 +123,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements group: '2_configuration', order: 1, }); - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { get title() { return getProfilesTitle(); }, @@ -111,6 +134,51 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements }); } + 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 readonly profilesDisposable = this._register(new MutableDisposable()); private registerProfilesActions(): void { this.profilesDisposable.value = new DisposableStore(); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts new file mode 100644 index 00000000000..4c294d03ac2 --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -0,0 +1,1067 @@ +/*--------------------------------------------------------------------------------------------- + * 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/userDataProfilesEditor'; +import { $, addDisposableListener, append, Dimension, EventHelper, EventType, IDomPosition, trackFocus } from 'vs/base/browser/dom'; +import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions'; +import { Event } from 'vs/base/common/event'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +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 { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IUserDataProfile, IUserDataProfilesService, ProfileResourceType } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorOpenContext, IEditorSerializer, IUntypedEditorInput } from 'vs/workbench/common/editor'; +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 { 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 { CancellationToken } from 'vs/base/common/cancellation'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; +import { Checkbox } from 'vs/base/browser/ui/toggle/toggle'; +import { DEFAULT_ICON, ICONS } from 'vs/workbench/services/userDataProfile/common/userDataProfileIcons'; +import { WorkbenchIconSelectBox } from 'vs/workbench/services/userDataProfile/browser/iconSelectBox'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IHoverService, WorkbenchHoverDelegate } from 'vs/platform/hover/browser/hover'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +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 { 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 { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { AbstractUserDataProfileElement, IProfileElement, 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 class UserDataProfilesEditor extends EditorPane implements IUserDataProfilesEditor { + + static readonly ID: string = 'workbench.editor.userDataProfiles'; + + private container: HTMLElement | undefined; + private splitView: SplitView | undefined; + private profilesTree: WorkbenchObjectTree | undefined; + private profileWidget: ProfileWidget | undefined; + + private model: UserDataProfilesEditorModel | undefined; + private templates: IProfileTemplateInfo[] = []; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @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, + ) { + super(UserDataProfilesEditor.ID, group, telemetryService, themeService, storageService); + } + + layout(dimension: Dimension, position?: IDomPosition | undefined): void { + if (this.container && this.splitView) { + const height = dimension.height - 20; + this.splitView.layout(this.container?.clientWidth, height); + this.splitView.el.style.height = `${height}px`; + } + } + + protected createEditor(parent: HTMLElement): void { + this.container = append(parent, $('.profiles-editor')); + + const sidebarView = append(this.container, $('.sidebar-view')); + const sidebarContainer = append(sidebarView, $('.sidebar-container')); + + const contentsView = append(this.container, $('.contents-view')); + const contentsContainer = append(contentsView, $('.contents-container')); + this.profileWidget = this._register(this.instantiationService.createInstance(ProfileWidget, contentsContainer)); + + this.splitView = new SplitView(this.container, { + orientation: Orientation.HORIZONTAL, + proportionalLayout: true + }); + + this.renderSidebar(sidebarContainer); + this.splitView.addView({ + onDidChange: Event.None, + element: sidebarView, + minimumSize: 175, + 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); + } + } + }, 300, undefined, true); + this.splitView.addView({ + onDidChange: Event.None, + element: contentsView, + minimumSize: 500, + maximumSize: Number.POSITIVE_INFINITY, + layout: (width, _, height) => { + contentsView.style.width = `${width}px`; + if (height) { + this.profileWidget?.layout(new Dimension(width, height)); + } + } + }, Sizing.Distribute, undefined, true); + + const borderColor = this.theme.getColor(profilesSashBorder)!; + this.splitView.style({ separatorBorder: borderColor }); + + this.registerListeners(); + + this.userDataProfileManagementService.getBuiltinProfileTemplates().then(templates => { + this.templates = templates; + this.profileWidget!.templates = templates; + }); + } + + 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')), + delegate, + [renderer], + { + multipleSelectionSupport: false, + setRowLineHeight: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(extensionFeature: IProfileElement | null): string { + return extensionFeature?.name ?? ''; + }, + getWidgetAriaLabel(): string { + return localize('profiles', "Profiles"); + } + }, + openOnSingleClick: true, + enableStickyScroll: false, + identityProvider: { + getId(e) { + if (e instanceof UserDataProfileElement) { + return e.profile.id; + } + return e.name; + } + } + })); + } + + private renderNewProfileButton(parent: HTMLElement): void { + const button = this._register(new ButtonWithDropdown(parent, { + actions: { + 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 Separator()); + } + actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); + return actions; + } + }, + addPrimaryActionToDropdown: false, + contextMenuProvider: this.contextMenuService, + supportIcons: true, + ...defaultButtonStyles + })); + button.label = `$(add) ${localize('newProfile', "New Profile")}`; + this._register(button.onDidClick(e => this.createNewProfile())); + } + + private registerListeners(): void { + if (this.profilesTree) { + this._register(this.profilesTree.onDidChangeSelection(e => { + const [element] = e.elements; + if (element instanceof AbstractUserDataProfileElement) { + this.profileWidget?.render(element); + } + })); + + this._register(this.profilesTree.onContextMenu(e => { + if (e.element instanceof AbstractUserDataProfileElement) { + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => e.element instanceof AbstractUserDataProfileElement ? e.element.contextMenuActions.slice(0) : [], + getActionsContext: () => e.element + }); + } + })); + } + } + + private async importProfile(): Promise { + const disposables = new DisposableStore(); + const quickPick = disposables.add(this.quickInputService.createQuickPick()); + + const updateQuickPickItems = (value?: string) => { + const quickPickItems: IQuickPickItem[] = []; + if (value) { + quickPickItems.push({ label: quickPick.value, description: localize('import from url', "Import from URL") }); + } + quickPickItems.push({ label: localize('import from file', "Select File...") }); + quickPick.items = quickPickItems; + }; + + quickPick.title = localize('import profile quick pick title', "Import from Profile Template..."); + quickPick.placeholder = localize('import profile placeholder', "Provide Profile Template URL"); + quickPick.ignoreFocusOut = true; + disposables.add(quickPick.onDidChangeValue(updateQuickPickItems)); + updateQuickPickItems(); + quickPick.matchOnLabel = false; + quickPick.matchOnDescription = false; + disposables.add(quickPick.onDidAccept(async () => { + quickPick.hide(); + const selectedItem = quickPick.selectedItems[0]; + if (!selectedItem) { + return; + } + const url = selectedItem.label === quickPick.value ? URI.parse(quickPick.value) : await this.getProfileUriFromFileSystem(); + if (url) { + this.createNewProfile(url); + } + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + } + + 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); + } + + private async getProfileUriFromFileSystem(): Promise { + const profileLocation = await this.fileDialogService.showOpenDialog({ + canSelectFolders: false, + canSelectFiles: true, + canSelectMany: false, + filters: PROFILE_FILTER, + title: localize('import profile dialog', "Select Profile Template File"), + }); + if (!profileLocation) { + return null; + } + return profileLocation[0]; + } + + 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._register(this.model.onDidChange((element) => { + this.updateProfilesTree(element); + })); + } + + override focus(): void { + super.focus(); + this.profilesTree?.domFocus(); + } + + private updateProfilesTree(elementToSelect?: IProfileElement): 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 + } + ]); + if (elementToSelect) { + this.profilesTree?.setSelection([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]); + } + } + } + } else { + const elementToSelect = this.model.profiles.find(profile => profile.active) ?? this.model.profiles[0]; + if (elementToSelect) { + this.profilesTree?.setSelection([elementToSelect]); + } + } + } + +} + +interface IProfileTreeElementTemplateData { + readonly icon: HTMLElement; + readonly label: HTMLElement; + readonly description: HTMLElement; + readonly disposables: DisposableStore; +} + +class ProfileTreeElementDelegate implements IListVirtualDelegate { + getHeight(element: IProfileElement) { + return 30; + } + getTemplateId() { return 'profileTreeElement'; } +} + +class ProfileTreeElementRenderer implements ITreeRenderer { + + readonly templateId = 'profileTreeElement'; + + 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')); + append(description, $(`span${ThemeIcon.asCSSSelector(Codicon.check)}`)); + append(description, $('span', undefined, localize('activeProfile', "Active"))); + return { label, icon, description, disposables: new DisposableStore() }; + } + + renderElement({ element }: ITreeNode, index: number, templateData: IProfileTreeElementTemplateData, height: number | undefined): void { + templateData.disposables.clear(); + templateData.label.textContent = element.name; + if (element.icon) { + templateData.icon.className = ThemeIcon.asClassName(ThemeIcon.fromId(element.icon)); + } else { + templateData.icon.className = 'hide'; + } + templateData.description.classList.toggle('hide', !element.active); + if (element.onDidChange) { + templateData.disposables.add(element.onDidChange(e => { + if (e.name) { + templateData.label.textContent = element.name; + } + if (e.icon) { + if (element.icon) { + templateData.icon.className = ThemeIcon.asClassName(ThemeIcon.fromId(element.icon)); + } else { + templateData.icon.className = 'hide'; + } + } + if (e.active) { + templateData.description.classList.toggle('hide', !element.active); + } + })); + } + } + + disposeTemplate(templateData: IProfileTreeElementTemplateData): void { + templateData.disposables.dispose(); + } +} + +class ProfileWidget extends Disposable { + + private readonly profileTitle: HTMLElement; + private readonly toolbar: WorkbenchToolBar; + private readonly buttonContainer: HTMLElement; + private readonly iconElement: HTMLElement; + private readonly nameContainer: HTMLElement; + private readonly nameInput: InputBox; + private readonly copyFromContainer: HTMLElement; + private readonly copyFromSelectBox: SelectBox; + private copyFromOptions: (ISelectOptionItem & { id?: string; source?: IUserDataProfile | URI })[] = []; + + private readonly resourcesTree: WorkbenchAsyncDataTree; + + private _templates: IProfileTemplateInfo[] = []; + public set templates(templates: IProfileTemplateInfo[]) { + this._templates = templates; + this.renderSelectBox(); + } + + private readonly _profileElement = this._register(new MutableDisposable<{ element: AbstractUserDataProfileElement } & IDisposable>()); + + constructor( + parent: HTMLElement, + @IHoverService private readonly hoverService: IHoverService, + @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") })); + this.renderIconSelectBox(this.iconElement); + + this.nameInput = this._register(new InputBox( + this.nameContainer, + undefined, + { + inputBoxStyles: defaultInputBoxStyles, + ariaLabel: localize('profileName', "Profile Name"), + placeholder: localize('profileName', "Profile Name"), + validationOptions: { + validation: (value) => { + if (!value) { + return { + content: localize('name required', "Profile name is required and must be a non-empty value."), + type: MessageType.ERROR + }; + } + const initialName = this._profileElement.value?.element instanceof UserDataProfileElement ? this._profileElement.value.element.profile.name : undefined; + if (initialName !== value && this.userDataProfilesService.profiles.some(p => p.name === value)) { + return { + content: localize('profileExists', "Profile with name {0} already exists.", value), + type: MessageType.ERROR + }; + } + return null; + } + } + } + )); + this.nameInput.onDidChange(value => { + if (this._profileElement.value && value) { + this._profileElement.value.element.name = value; + } + }); + const focusTracker = this._register(trackFocus(this.nameInput.inputElement)); + this._register(focusTracker.onDidBlur(() => { + if (this._profileElement.value && !this.nameInput.value) { + this.nameInput.value = this._profileElement.value.element.name; + } + })); + + this.copyFromContainer = append(body, $('.profile-copy-from-container')); + append(this.copyFromContainer, $('.profile-copy-from-label', undefined, localize('create from', "Copy from:"))); + this.copyFromSelectBox = this._register(this.instantiationService.createInstance(SelectBox, + [], + 0, + this.contextViewService, + defaultSelectBoxStyles, + { + useCustomDrawn: true, + ariaLabel: localize('copy profile from', "Copy profile from"), + } + )); + 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"))); + + const delegate = new ProfileResourceTreeElementDelegate(); + this.resourcesTree = this._register(this.instantiationService.createInstance(WorkbenchAsyncDataTree, + 'ProfileEditor-ResourcesTree', + append(body, $('.profile-content-tree.file-icon-themable-tree.show-file-icons')), + delegate, + [ + this.instantiationService.createInstance(ExistingProfileResourceTreeRenderer), + this.instantiationService.createInstance(NewProfileResourceTreeRenderer), + this.instantiationService.createInstance(ProfileResourceChildTreeItemRenderer), + ], + this.instantiationService.createInstance(ProfileResourceTreeDataSource), + { + multipleSelectionSupport: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(element: ProfileResourceTreeElement | null): string { + if (isString(element?.element)) { + return element.element; + } + if (element?.element) { + return element.element.label?.label ?? ''; + } + return ''; + }, + getWidgetAriaLabel(): string { + return ''; + }, + }, + identityProvider: { + getId(element) { + if (isString(element?.element)) { + return element.element; + } + if (element?.element) { + return element.element.handle; + } + return ''; + } + }, + expandOnlyOnTwistieClick: true, + renderIndentGuides: RenderIndentGuides.None, + openOnSingleClick: true, + enableStickyScroll: false, + })); + this._register(this.resourcesTree.onDidOpen(async (e) => { + if (!e.browserEvent) { + return; + } + 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); + } + } + })); + } + + private renderIconSelectBox(iconContainer: HTMLElement): void { + const iconSelectBox = this._register(this.instantiationService.createInstance(WorkbenchIconSelectBox, { icons: ICONS, inputBoxStyles: defaultInputBoxStyles })); + let hoverWidget: IHoverWidget | undefined; + const showIconSelectBox = () => { + if (this._profileElement.value?.element instanceof UserDataProfileElement && this._profileElement.value.element.profile.isDefault) { + return; + } + iconSelectBox.clearInput(); + hoverWidget = this.hoverService.showHover({ + content: iconSelectBox.domNode, + target: iconContainer, + position: { + hoverPosition: HoverPosition.BELOW, + }, + persistence: { + sticky: true, + }, + appearance: { + showPointer: true, + }, + }, true); + + if (hoverWidget) { + iconSelectBox.layout(new Dimension(486, 260)); + iconSelectBox.focus(); + } + }; + this._register(addDisposableListener(iconContainer, EventType.CLICK, (e: MouseEvent) => { + EventHelper.stop(e, true); + showIconSelectBox(); + })); + this._register(addDisposableListener(iconContainer, EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { + EventHelper.stop(event, true); + showIconSelectBox(); + } + })); + this._register(addDisposableListener(iconSelectBox.domNode, EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Escape)) { + EventHelper.stop(event, true); + hoverWidget?.dispose(); + iconContainer.focus(); + } + })); + this._register(iconSelectBox.onDidSelect(selectedIcon => { + hoverWidget?.dispose(); + iconContainer.focus(); + if (this._profileElement.value) { + this._profileElement.value.element.icon = selectedIcon.id; + } + })); + } + + 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._register(this.copyFromSelectBox.onDidSelect(option => { + if (this._profileElement.value?.element instanceof NewProfileElement) { + this._profileElement.value.element.copyFrom = this.copyFromOptions[option.index].source; + } + })); + } + + layout(dimension: Dimension): void { + this.resourcesTree.layout(dimension.height - 34 - 20 - 25 - 20, dimension.width); + } + + render(profileElement: AbstractUserDataProfileElement): void { + const disposables = new DisposableStore(); + this._profileElement.value = { element: profileElement, dispose: () => disposables.dispose() }; + + this.renderProfileElement(profileElement); + disposables.add(profileElement.onDidChange(e => this.renderProfileElement(profileElement))); + + const profile = profileElement instanceof UserDataProfileElement ? profileElement.profile : undefined; + this.nameInput.setEnabled(!profile?.isDefault); + + this.resourcesTree.setInput(profileElement); + disposables.add(profileElement.onDidChange(e => { + if (e.flags || e.copyFrom) { + const viewState = this.resourcesTree.getViewState(); + this.resourcesTree.setInput(profileElement, { + ...viewState, + expanded: viewState.expanded?.map(e => e) + }); + } + })); + + if (profileElement.primaryAction) { + 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; + } + })); + disposables.add(profileElement.onDidChange(e => { + if (e.message) { + button.setTitle(profileElement.message ?? profileElement.primaryAction!.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.select(); + } + } + + private renderProfileElement(profileElement: AbstractUserDataProfileElement): void { + this.profileTitle.textContent = profileElement.name; + this.nameInput.value = profileElement.name; + 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.copyFromContainer.classList.remove('hide'); + 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.select(index); + } else { + this.copyFromSelectBox.setOptions([{ text: basename(profileElement.copyFrom as URI) }]); + this.copyFromSelectBox.setEnabled(false); + } + } else { + this.copyFromContainer.classList.add('hide'); + } + } +} + + +interface ProfileResourceTreeElement { + element: ProfileResourceType | IProfileResourceChildTreeItem; + root: AbstractUserDataProfileElement; +} + +class ProfileResourceTreeElementDelegate implements IListVirtualDelegate { + getTemplateId(element: ProfileResourceTreeElement) { + if (!isString(element.element)) { + return ProfileResourceChildTreeItemRenderer.TEMPLATE_ID; + } + if (element.root instanceof NewProfileElement) { + return NewProfileResourceTreeRenderer.TEMPLATE_ID; + } + return ExistingProfileResourceTreeRenderer.TEMPLATE_ID; + } + getHeight(element: ProfileResourceTreeElement) { + return 30; + } +} + +class ProfileResourceTreeDataSource implements IAsyncDataSource { + + constructor( + @IEditorProgressService private readonly editorProgressService: IEditorProgressService, + ) { } + + hasChildren(element: AbstractUserDataProfileElement | ProfileResourceTreeElement): boolean { + if (element instanceof AbstractUserDataProfileElement) { + return true; + } + if (isString(element.element)) { + if (element.root.getFlag(element.element)) { + return false; + } + if (element.root instanceof NewProfileElement) { + return element.root.copyFrom !== undefined; + } + return true; + } + return false; + } + + async getChildren(element: AbstractUserDataProfileElement | ProfileResourceTreeElement): Promise { + if (element instanceof AbstractUserDataProfileElement) { + const resourceTypes = [ + ProfileResourceType.Settings, + ProfileResourceType.Keybindings, + ProfileResourceType.Snippets, + ProfileResourceType.Tasks, + ProfileResourceType.Extensions + ]; + return resourceTypes.map(resourceType => ({ element: resourceType, root: element })); + } + if (isString(element.element)) { + const progressRunner = this.editorProgressService.show(true); + try { + const extensions = await element.root.getChildren(element.element); + return extensions.map(extension => ({ element: extension, root: element.root })); + } finally { + progressRunner.done(); + } + } + return []; + } +} + +interface IProfileResourceTemplateData { + readonly disposables: DisposableStore; + readonly elementDisposables: DisposableStore; +} + +interface IExistingProfileResourceTemplateData extends IProfileResourceTemplateData { + readonly checkbox: Checkbox; + readonly label: HTMLElement; + readonly description: HTMLElement; +} + +interface INewProfileResourceTemplateData extends IProfileResourceTemplateData { + readonly label: HTMLElement; + readonly selectContainer: HTMLElement; + readonly selectBox: SelectBox; +} + +interface IProfileResourceChildTreeItemTemplateData extends IProfileResourceTemplateData { + readonly checkbox: Checkbox; + readonly resourceLabel: IResourceLabel; +} + +class AbstractProfileResourceTreeRenderer extends Disposable { + + protected getResourceTypeTitle(resourceType: ProfileResourceType): string { + switch (resourceType) { + case ProfileResourceType.Settings: + return localize('settings', "Settings"); + case ProfileResourceType.Keybindings: + return localize('keybindings', "Keyboard Shortcuts"); + case ProfileResourceType.Snippets: + return localize('snippets', "User Snippets"); + case ProfileResourceType.Tasks: + return localize('tasks', "User Tasks"); + case ProfileResourceType.Extensions: + return localize('extensions', "Extensions"); + } + return ''; + } + + disposeElement(element: ITreeNode, index: number, templateData: IProfileResourceTemplateData, height: number | undefined): void { + templateData.elementDisposables.clear(); + } + + disposeTemplate(templateData: IProfileResourceTemplateData): void { + templateData.disposables.dispose(); + } +} + + +class ExistingProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer implements ITreeRenderer { + + static readonly TEMPLATE_ID = 'ExistingProfileResourceTemplate'; + + readonly templateId = ExistingProfileResourceTreeRenderer.TEMPLATE_ID; + + renderTemplate(parent: HTMLElement): IExistingProfileResourceTemplateData { + const disposables = new DisposableStore(); + const container = append(parent, $('.profile-tree-item-container.existing-profile-resource-type-container')); + const checkbox = disposables.add(new Checkbox('', false, defaultCheckboxStyles)); + append(container, checkbox.domNode); + const label = append(container, $('.profile-resource-type-label')); + const description = append(container, $('.profile-resource-type-description', undefined, localize('using defaults', "Using Default Profile"))); + return { checkbox, label, description, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + } + + renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: IExistingProfileResourceTemplateData, height: number | undefined): void { + templateData.elementDisposables.clear(); + const { element, root } = profileResourceTreeElement; + 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'); + } + + templateData.label.textContent = this.getResourceTypeTitle(element); + if (root instanceof UserDataProfileElement && root.profile.isDefault) { + templateData.checkbox.checked = true; + templateData.checkbox.disable(); + templateData.description.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)); + } + })); + } + } + +} + +class NewProfileResourceTreeRenderer extends AbstractProfileResourceTreeRenderer implements ITreeRenderer { + + static readonly TEMPLATE_ID = 'NewProfileResourceTemplate'; + + readonly templateId = NewProfileResourceTreeRenderer.TEMPLATE_ID; + + constructor( + @IContextViewService private readonly contextViewService: IContextViewService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + } + + renderTemplate(parent: HTMLElement): INewProfileResourceTemplateData { + const disposables = new DisposableStore(); + const container = append(parent, $('.profile-tree-item-container.new-profile-resource-type-container')); + 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, + { + useCustomDrawn: true, + } + )); + const selectContainer = append(container, $('.profile-select-container')); + selectBox.render(selectContainer); + + return { label, selectContainer, selectBox, disposables, elementDisposables: disposables.add(new DisposableStore()) }; + } + + renderElement({ element: profileResourceTreeElement }: ITreeNode, index: number, templateData: INewProfileResourceTemplateData, height: number | undefined): void { + templateData.elementDisposables.clear(); + const { element, root } = profileResourceTreeElement; + 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'); + } + 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); + })); + } +} + +class ProfileResourceChildTreeItemRenderer extends AbstractProfileResourceTreeRenderer implements ITreeRenderer { + + static readonly TEMPLATE_ID = 'ProfileResourceChildTreeItemTemplate'; + + readonly templateId = ProfileResourceChildTreeItemRenderer.TEMPLATE_ID; + private readonly labels: ResourceLabels; + private readonly hoverDelegate: IHoverDelegate; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this.labels = instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER); + this.hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', false, {})); + } + + renderTemplate(parent: HTMLElement): IProfileResourceChildTreeItemTemplateData { + const disposables = new DisposableStore(); + const container = append(parent, $('.profile-tree-item-container.profile-resource-child-container')); + 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()) }; + } + + 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 (element.checkbox) { + templateData.checkbox.domNode.classList.remove('hide'); + templateData.checkbox.checked = element.checkbox.isChecked; + templateData.checkbox.domNode.ariaLabel = element.checkbox.accessibilityInformation?.label ?? ''; + if (element.checkbox.accessibilityInformation?.role) { + templateData.checkbox.domNode.role = element.checkbox.accessibilityInformation.role; + } + } else { + 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 + }, + { + forceLabel: true, + hideIcon: !resource, + }); + } + +} + +export class UserDataProfilesEditorInput extends EditorInput { + static readonly ID: string = 'workbench.input.userDataProfiles'; + readonly resource = undefined; + + private readonly model: UserDataProfilesEditorModel; + + private _dirty: boolean = false; + get dirty(): boolean { return this._dirty; } + set dirty(dirty: boolean) { + if (this._dirty !== dirty) { + this._dirty = dirty; + this._onDidChangeDirty.fire(); + } + } + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this.model = UserDataProfilesEditorModel.getInstance(this.instantiationService); + this._register(this.model.onDidChange(e => this.dirty = this.model.profiles.some(profile => profile instanceof NewProfileElement))); + } + + override get typeId(): string { return UserDataProfilesEditorInput.ID; } + override getName(): string { return localize('userDataProfiles', "Profiles"); } + override getIcon(): ThemeIcon | undefined { return defaultUserDataProfileIcon; } + + override async resolve(): Promise { + return this.model; + } + + override isDirty(): boolean { + return this.dirty; + } + + override async save(): Promise { + await this.model.saveNewProfile(); + return this; + } + + override async revert(): Promise { + this.model.revert(); + } + + override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { return otherInput instanceof UserDataProfilesEditorInput; } +} + +export class UserDataProfilesEditorInputSerializer implements IEditorSerializer { + canSerialize(editorInput: EditorInput): boolean { return true; } + serialize(editorInput: EditorInput): string { return ''; } + deserialize(instantiationService: IInstantiationService): EditorInput { return instantiationService.createInstance(UserDataProfilesEditorInput); } +} diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts new file mode 100644 index 00000000000..bde549ecefa --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -0,0 +1,586 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action, IAction, Separator } from 'vs/base/common/actions'; +import { Emitter, Event } 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'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { DidChangeProfilesEvent, isUserDataProfile, IUserDataProfile, IUserDataProfilesService, ProfileResourceType, ProfileResourceTypeFlags, toUserDataProfile, UseDefaultProfileFlags } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IProfileResourceChildTreeItem, IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService, IUserDataProfileTemplate } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { equals } from 'vs/base/common/objects'; +import { EditorModel } from 'vs/workbench/common/editor/editorModel'; +import { ExtensionsResourceExportTreeItem, ExtensionsResourceImportTreeItem } from 'vs/workbench/services/userDataProfile/browser/extensionsResource'; +import { SettingsResource, SettingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/settingsResource'; +import { KeybindingsResource, KeybindingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/keybindingsResource'; +import { TasksResource, TasksResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/tasksResource'; +import { SnippetsResource, SnippetsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/snippetsResource'; +import { Codicon } from 'vs/base/common/codicons'; +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'; + +export type ChangeEvent = { + readonly name?: boolean; + readonly icon?: boolean; + readonly flags?: boolean; + readonly active?: boolean; + readonly message?: boolean; + readonly copyFrom?: boolean; + readonly copyFlags?: boolean; +}; + +export interface IProfileElement { + readonly onDidChange?: Event; + readonly name: string; + readonly icon?: string; + readonly flags?: UseDefaultProfileFlags; + readonly active?: boolean; + readonly message?: string; +} + +export abstract class AbstractUserDataProfileElement extends Disposable { + + protected readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + constructor( + name: string, + icon: string | undefined, + flags: UseDefaultProfileFlags | undefined, + isActive: boolean, + @IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + ) { + super(); + this._name = name; + this._icon = icon; + this._flags = flags; + this._active = isActive; + this._register(this.onDidChange(e => { + if (!e.message) { + this.validate(); + } + if (this.primaryAction) { + this.primaryAction.enabled = !this.message; + } + })); + } + + private _name = ''; + get name(): string { return this._name; } + set name(label: string) { + if (this._name !== label) { + this._name = label; + this._onDidChange.fire({ name: true }); + } + } + + private _icon: string | undefined; + get icon(): string | undefined { return this._icon; } + set icon(icon: string | undefined) { + if (this._icon !== icon) { + this._icon = icon; + this._onDidChange.fire({ icon: true }); + } + } + + private _flags: UseDefaultProfileFlags | undefined; + get flags(): UseDefaultProfileFlags | undefined { return this._flags; } + set flags(flags: UseDefaultProfileFlags | undefined) { + if (!equals(this._flags, flags)) { + this._flags = flags; + this._onDidChange.fire({ flags: true }); + } + } + + private _active: boolean = false; + get active(): boolean { return this._active; } + set active(active: boolean) { + if (this._active !== active) { + this._active = active; + this._onDidChange.fire({ active: true }); + } + } + + private _message: string | undefined; + get message(): string | undefined { return this._message; } + set message(message: string | undefined) { + if (this._message !== message) { + this._message = message; + this._onDidChange.fire({ message: true }); + } + } + + getFlag(key: ProfileResourceType): boolean { + return this.flags?.[key] ?? false; + } + + setFlag(key: ProfileResourceType, value: boolean): void { + const flags = this.flags ? { ...this.flags } : {}; + if (value) { + flags[key] = true; + } else { + delete flags[key]; + } + this.flags = flags; + } + + validate(): void { + if (!this.name) { + this.message = localize('profileNameRequired', "Profile name is required."); + return; + } + if (this.name !== this.getInitialName() && this.userDataProfilesService.profiles.some(p => p.name === this.name)) { + this.message = localize('profileExists', "Profile with name {0} already exists.", this.name); + return; + } + if ( + this.flags && this.flags.settings && this.flags.keybindings && this.flags.tasks && this.flags.snippets && this.flags.extensions + ) { + this.message = localize('invalid configurations', "The profile should contain at least one configuration."); + return; + } + this.message = undefined; + } + + async getChildren(resourceType: ProfileResourceType): Promise { + return []; + } + + protected async getChildrenFromProfile(profile: IUserDataProfile, resourceType: ProfileResourceType): Promise { + profile = this.getFlag(resourceType) ? this.userDataProfilesService.defaultProfile : profile; + switch (resourceType) { + case ProfileResourceType.Settings: + return this.instantiationService.createInstance(SettingsResourceTreeItem, profile).getChildren(); + case ProfileResourceType.Keybindings: + return this.instantiationService.createInstance(KeybindingsResourceTreeItem, profile).getChildren(); + case ProfileResourceType.Snippets: + return (await this.instantiationService.createInstance(SnippetsResourceTreeItem, profile).getChildren()) ?? []; + case ProfileResourceType.Tasks: + return this.instantiationService.createInstance(TasksResourceTreeItem, profile).getChildren(); + case ProfileResourceType.Extensions: + return this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, profile).getChildren(); + } + return []; + } + + protected getInitialName(): string { + return ''; + } + + abstract readonly primaryAction?: Action; + abstract readonly titleActions: [IAction[], IAction[]]; + abstract readonly contextMenuActions: IAction[]; +} + +export class UserDataProfileElement extends AbstractUserDataProfileElement implements IProfileElement { + + get profile(): IUserDataProfile { return this._profile; } + + readonly primaryAction = undefined; + + private readonly saveScheduler = this._register(new RunOnceScheduler(() => this.doSave(), 500)); + + constructor( + private _profile: IUserDataProfile, + readonly titleActions: [IAction[], IAction[]], + readonly contextMenuActions: IAction[], + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super( + _profile.name, + _profile.icon, + _profile.useDefaultFlags, + userDataProfileService.currentProfile.id === _profile.id, + userDataProfilesService, + instantiationService, + ); + this._register(this.userDataProfileService.onDidChangeCurrentProfile(() => this.active = this.userDataProfileService.currentProfile.id === this.profile.id)); + this._register(this.userDataProfilesService.onDidChangeProfiles(() => { + const profile = this.userDataProfilesService.profiles.find(p => p.id === this.profile.id); + if (profile) { + this._profile = profile; + this.name = profile.name; + this.icon = profile.icon; + this.flags = profile.useDefaultFlags; + } + })); + this._register(this.onDidChange(e => { + this.save(); + })); + } + + private hasUnsavedChanges(): boolean { + if (this.name !== this.profile.name) { + return true; + } + if (this.icon !== this.profile.icon) { + return true; + } + if (!equals(this.flags ?? {}, this.profile.useDefaultFlags ?? {})) { + return true; + } + return false; + } + + save(): void { + this.saveScheduler.schedule(); + } + + private async doSave(): Promise { + if (!this.hasUnsavedChanges()) { + 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; + + await this.userDataProfileManagementService.updateProfile(this.profile, { + name: this.name, + icon: this.icon, + useDefaultFlags: this.profile.useDefaultFlags && !useDefaultFlags ? {} : useDefaultFlags + }); + } + + override async getChildren(resourceType: ProfileResourceType): Promise { + return this.getChildrenFromProfile(this.profile, resourceType); + } + + protected override getInitialName(): string { + return this.profile.name; + } + +} + +const USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME = 'userdataprofiletemplatepreview'; + +export class NewProfileElement extends AbstractUserDataProfileElement implements IProfileElement { + + constructor( + name: string, + copyFrom: URI | IUserDataProfile | undefined, + readonly primaryAction: Action, + readonly titleActions: [IAction[], IAction[]], + readonly contextMenuActions: Action[], + @IFileService private readonly fileService: IFileService, + @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super( + name, + undefined, + undefined, + false, + userDataProfilesService, + instantiationService, + ); + this._copyFrom = copyFrom; + this._copyFlags = this.getCopyFlagsFrom(copyFrom); + this._register(this.fileService.registerProvider(USER_DATA_PROFILE_TEMPLATE_PREVIEW_SCHEME, this._register(new InMemoryFileSystemProvider()))); + } + + private _copyFrom: IUserDataProfile | URI | undefined; + get copyFrom(): IUserDataProfile | URI | undefined { return this._copyFrom; } + set copyFrom(copyFrom: IUserDataProfile | URI | undefined) { + if (this._copyFrom !== copyFrom) { + this._copyFrom = copyFrom; + this._onDidChange.fire({ copyFrom: true }); + this.flags = undefined; + this.copyFlags = this.getCopyFlagsFrom(copyFrom); + } + } + + private _copyFlags: ProfileResourceTypeFlags | undefined; + get copyFlags(): ProfileResourceTypeFlags | undefined { return this._copyFlags; } + set copyFlags(flags: ProfileResourceTypeFlags | undefined) { + if (!equals(this._copyFlags, flags)) { + this._copyFlags = flags; + this._onDidChange.fire({ copyFlags: true }); + } + } + + private getCopyFlagsFrom(copyFrom: URI | IUserDataProfile | undefined): ProfileResourceTypeFlags | undefined { + return copyFrom ? { + settings: true, + keybindings: true, + snippets: true, + tasks: true, + extensions: true + } : undefined; + } + + getCopyFlag(key: ProfileResourceType): boolean { + return this.copyFlags?.[key] ?? false; + } + + setCopyFlag(key: ProfileResourceType, value: boolean): void { + const flags = this.copyFlags ? { ...this.copyFlags } : {}; + flags[key] = value; + this.copyFlags = flags; + } + + override async getChildren(resourceType: ProfileResourceType): Promise { + if (!this.getCopyFlag(resourceType)) { + return []; + } + if (this.copyFrom instanceof URI) { + const template = await this.userDataProfileImportExportService.resolveProfileTemplate(this.copyFrom); + if (!template) { + return []; + } + return this.getChildrenFromProfileTemplate(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 { + 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); + case ProfileResourceType.Keybindings: + if (profileTemplate.keybindings) { + await this.instantiationService.createInstance(KeybindingsResource).apply(profileTemplate.keybindings, profile); + } + return this.getChildrenFromProfile(profile, resourceType); + case ProfileResourceType.Snippets: + if (profileTemplate.snippets) { + await this.instantiationService.createInstance(SnippetsResource).apply(profileTemplate.snippets, profile); + } + return this.getChildrenFromProfile(profile, resourceType); + case ProfileResourceType.Tasks: + if (profileTemplate.tasks) { + await this.instantiationService.createInstance(TasksResource).apply(profileTemplate.tasks, profile); + } + return this.getChildrenFromProfile(profile, resourceType); + case ProfileResourceType.Extensions: + if (profileTemplate.extensions) { + return this.instantiationService.createInstance(ExtensionsResourceImportTreeItem, profileTemplate.extensions).getChildren(); + } + } + return []; + } +} + +export class UserDataProfilesEditorModel extends EditorModel { + + private static INSTANCE: UserDataProfilesEditorModel | undefined; + static getInstance(instantiationService: IInstantiationService): UserDataProfilesEditorModel { + if (!UserDataProfilesEditorModel.INSTANCE) { + UserDataProfilesEditorModel.INSTANCE = instantiationService.createInstance(UserDataProfilesEditorModel); + } + return UserDataProfilesEditorModel.INSTANCE; + } + + private _profiles: [AbstractUserDataProfileElement, DisposableStore][] = []; + get profiles(): AbstractUserDataProfileElement[] { + return this._profiles + .map(([profile]) => profile) + .sort((a, b) => { + if (a instanceof NewProfileElement) { + return 1; + } + if (b instanceof NewProfileElement) { + return -1; + } + if (a instanceof UserDataProfileElement && a.profile.isDefault) { + return -1; + } + if (b instanceof UserDataProfileElement && b.profile.isDefault) { + return 1; + } + return a.name.localeCompare(b.name); + }); + } + + private newProfileElement: NewProfileElement | undefined; + + private _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + constructor( + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IUserDataProfileImportExportService private readonly userDataProfileImportExportService: IUserDataProfileImportExportService, + @IDialogService private readonly dialogService: IDialogService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + for (const profile of userDataProfilesService.profiles) { + 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 { + for (const profile of e.added) { + if (profile.name !== this.newProfileElement?.name) { + this._profiles.push(this.createProfileElement(profile)); + } + } + for (const profile of e.removed) { + const index = this._profiles.findIndex(([p]) => p instanceof UserDataProfileElement && p.profile.id === profile.id); + if (index !== -1) { + this._profiles.splice(index, 1).map(([, disposables]) => disposables.dispose()); + } + } + 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 titlePrimaryActions: IAction[] = []; + titlePrimaryActions.push(activateAction); + titlePrimaryActions.push(exportAction); + if (!profile.isDefault) { + titlePrimaryActions.push(deleteAction); + } + + const titleSecondaryActions: IAction[] = []; + titleSecondaryActions.push(copyFromProfileAction); + + const secondaryActions: IAction[] = []; + secondaryActions.push(activateAction); + secondaryActions.push(new Separator()); + secondaryActions.push(copyFromProfileAction); + secondaryActions.push(exportAction); + if (!profile.isDefault) { + secondaryActions.push(new Separator()); + secondaryActions.push(deleteAction); + } + const profileElement = disposables.add(this.instantiationService.createInstance(UserDataProfileElement, + profile, + [titlePrimaryActions, titleSecondaryActions], + secondaryActions, + )); + return [profileElement, disposables]; + } + + createNewProfile(copyFrom?: URI | IUserDataProfile): IProfileElement { + if (!this.newProfileElement) { + const disposables = new DisposableStore(); + const discardAction = disposables.add(new Action('userDataProfile.discard', localize('discard', "Discard"), ThemeIcon.asClassName(Codicon.close), true, () => { + this.removeNewProfile(); + this._onDidChange.fire(undefined); + })); + this.newProfileElement = disposables.add(this.instantiationService.createInstance(NewProfileElement, + localize('untitled', "Untitled"), + copyFrom, + disposables.add(new Action('userDataProfile.create', localize('create', "Create & Apply"), undefined, true, () => this.saveNewProfile())), + [[discardAction], []], + [discardAction], + )); + this._profiles.push([this.newProfileElement, disposables]); + this._onDidChange.fire(this.newProfileElement); + } + return this.newProfileElement; + } + + revert(): void { + this.removeNewProfile(); + this._onDidChange.fire(undefined); + } + + private removeNewProfile(): void { + if (this.newProfileElement) { + const index = this._profiles.findIndex(([p]) => p === this.newProfileElement); + if (index !== -1) { + this._profiles.splice(index, 1).map(([, disposables]) => disposables.dispose()); + } + this.newProfileElement = undefined; + } + } + + async saveNewProfile(): Promise { + if (!this.newProfileElement) { + return; + } + this.newProfileElement.validate(); + if (this.newProfileElement.message) { + return; + } + const { flags, icon, name, copyFrom } = this.newProfileElement; + const useDefaultFlags: UseDefaultProfileFlags | undefined = flags + ? flags.settings && flags.keybindings && flags.tasks && flags.globalState && flags.extensions ? undefined : flags + : undefined; + + 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.removeNewProfile(); + const profile = this.userDataProfilesService.profiles.find(p => p.name === name); + if (profile) { + this.onDidChangeProfiles({ added: [profile], removed: [], updated: [], all: this.userDataProfilesService.profiles }); + } + } + + private async removeProfile(profile: IUserDataProfile): Promise { + const result = await this.dialogService.confirm({ + type: 'info', + message: localize('deleteProfile', "Are you sure you want to delete the profile '{0}'?", profile.name), + primaryButton: localize('delete', "Delete"), + cancelButton: localize('cancel', "Cancel") + }); + if (result.confirmed) { + await this.userDataProfileManagementService.removeProfile(profile); + } + } + + private async exportProfile(profile: IUserDataProfile): Promise { + return this.userDataProfileImportExportService.exportProfile2(profile); + } +} diff --git a/src/vs/workbench/contrib/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/common/userDataProfile.ts new file mode 100644 index 00000000000..d816f4ee1c3 --- /dev/null +++ b/src/vs/workbench/contrib/userDataProfile/common/userDataProfile.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEditorPane } from 'vs/workbench/common/editor'; + +export interface IUserDataProfilesEditor extends IEditorPane { + +} diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 632ee6ad4e3..c2de392a001 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -730,15 +730,15 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo f1: true, precondition: when, menu: [{ - group: '3_settings_sync', + group: '3_configuration', id: MenuId.GlobalActivity, when, - order: 1 + order: 2 }, { - group: '3_settings_sync', + group: '3_configuration', id: MenuId.MenubarPreferencesMenu, when, - order: 1 + order: 2 }, { group: '1_settings', id: MenuId.AccountsContext, @@ -762,7 +762,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo title: localize('turnin on sync', "Turning on Settings Sync..."), precondition: ContextKeyExpr.false(), menu: [{ - group: '3_settings_sync', + group: '3_configuration', id: MenuId.GlobalActivity, when, order: 2 @@ -809,7 +809,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo id: 'workbench.userData.actions.signin', title: localize('sign in global', "Sign in to Sync Settings"), menu: { - group: '3_settings_sync', + group: '3_configuration', id: MenuId.GlobalActivity, when, order: 2 @@ -851,12 +851,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo f1: true, precondition: CONTEXT_HAS_CONFLICTS, menu: [{ - group: '3_settings_sync', + group: '3_configuration', id: MenuId.GlobalActivity, when: CONTEXT_HAS_CONFLICTS, order: 2 }, { - group: '3_settings_sync', + group: '3_configuration', id: MenuId.MenubarPreferencesMenu, when: CONTEXT_HAS_CONFLICTS, order: 2 @@ -881,13 +881,13 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo menu: [ { id: MenuId.GlobalActivity, - group: '3_settings_sync', + group: '3_configuration', when, order: 2 }, { id: MenuId.MenubarPreferencesMenu, - group: '3_settings_sync', + group: '3_configuration', when, order: 2, }, diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts index 37c0a56e082..890ffcf50db 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncConflictsView.ts @@ -92,7 +92,7 @@ export class UserDataSyncConflictsViewPane extends TreeViewPane implements IUser label: { label: basename(resource.remoteResource), strikethrough: resource.mergeState === MergeState.Accepted && (resource.localChange === Change.Deleted || resource.remoteChange === Change.Deleted) }, description: getSyncAreaLabel(resource.syncResource), collapsibleState: TreeItemCollapsibleState.None, - command: { id: `workbench.actions.sync.openConflicts`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle }] }, + command: { id: `workbench.actions.sync.openConflicts`, title: '', arguments: [{ $treeViewId: '', $treeItemHandle: handle } satisfies TreeViewItemHandleArg] }, contextValue: `sync-conflict-resource` }; children.push(treeItem); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 34da3b15c2a..22e8f21e168 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -61,7 +61,7 @@ export class UserDataSyncDataViews extends Disposable { private registerConflictsView(container: ViewContainer): void { const viewsRegistry = Registry.as(Extensions.ViewsRegistry); const viewName = localize2('conflicts', "Conflicts"); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id: SYNC_CONFLICTS_VIEW_ID, name: viewName, ctorDescriptor: new SyncDescriptor(UserDataSyncConflictsViewPane), @@ -71,7 +71,8 @@ export class UserDataSyncDataViews extends Disposable { treeView: this.instantiationService.createInstance(TreeView, SYNC_CONFLICTS_VIEW_ID, viewName.value), collapsed: false, order: 100, - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); } private registerMachinesView(container: ViewContainer): void { @@ -85,7 +86,7 @@ export class UserDataSyncDataViews extends Disposable { this._register(Event.any(this.userDataSyncMachinesService.onDidChange, this.userDataSyncService.onDidResetRemote)(() => treeView.refresh())); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), @@ -95,7 +96,8 @@ export class UserDataSyncDataViews extends Disposable { treeView, collapsed: false, order: 300, - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); this._register(registerAction2(class extends Action2 { constructor() { @@ -152,7 +154,7 @@ export class UserDataSyncDataViews extends Disposable { this.userDataSyncService.onDidResetLocal, this.userDataSyncService.onDidResetRemote)(() => treeView.refresh())); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), @@ -163,7 +165,8 @@ export class UserDataSyncDataViews extends Disposable { collapsed: false, order: remote ? 200 : 400, hideByDefault: !remote, - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); this.registerDataViewActions(id); } @@ -178,7 +181,7 @@ export class UserDataSyncDataViews extends Disposable { treeView.dataProvider = dataProvider; const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), @@ -188,7 +191,8 @@ export class UserDataSyncDataViews extends Disposable { treeView, collapsed: false, hideByDefault: false, - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); this._register(registerAction2(class extends Action2 { constructor() { @@ -303,7 +307,7 @@ export class UserDataSyncDataViews extends Disposable { treeView.dataProvider = dataProvider; const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([{ + const viewDescriptor: ITreeViewDescriptor = { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), @@ -314,7 +318,8 @@ export class UserDataSyncDataViews extends Disposable { collapsed: false, order: 500, hideByDefault: true - }], container); + }; + viewsRegistry.registerViews([viewDescriptor], container); } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 51c08c3158b..6ee778a292b 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -431,7 +431,8 @@ class TrustedUriActionsColumnRenderer implements ITableRenderer{ + return { + label: '', class: ThemeIcon.asClassName(editIcon), enabled: true, id: 'editTrustedUri', @@ -443,7 +444,8 @@ class TrustedUriActionsColumnRenderer implements ITableRenderer{ + return { + label: '', class: ThemeIcon.asClassName(folderPickerIcon), enabled: true, id: 'pickerTrustedUri', @@ -455,7 +457,8 @@ class TrustedUriActionsColumnRenderer implements ITableRenderer{ + return { + label: '', class: ThemeIcon.asClassName(removeIcon), enabled: true, id: 'deleteTrustedUri', diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index ff0aeec2494..2115ddfecc0 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -356,6 +356,10 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from 'vs/platform/window/electron-sand type: 'string', description: localize('argv.locale', 'The display Language to use. Picking a different language requires the associated language pack to be installed.') }, + 'disable-lcd-text': { + type: 'boolean', + description: localize('argv.disableLcdText', 'Disables LCD font antialiasing.') + }, '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 dfbe243f525..73ebdb1b2a9 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -196,6 +196,18 @@ export class NativeWindow extends BaseWindow { } }); + // Shared Process crash reported from main + ipcRenderer.on('vscode:reportSharedProcessCrash', (event: unknown, error: string) => { + this.notificationService.prompt( + Severity.Error, + localize('sharedProcessCrash', "A shared background process terminated unexpectedly. Please restart the application to recover."), + [{ + label: localize('restart', "Restart"), + run: () => this.nativeHostService.relaunch() + }] + ); + }); + // Support openFiles event for existing and new files ipcRenderer.on('vscode:openFiles', (event: unknown, request: IOpenFileRequest) => { this.onOpenFiles(request); }); @@ -253,8 +265,8 @@ export class NativeWindow extends BaseWindow { }); // Fullscreen Events - ipcRenderer.on('vscode:enterFullScreen', async () => { setFullscreen(true, mainWindow); }); - ipcRenderer.on('vscode:leaveFullScreen', async () => { setFullscreen(false, mainWindow); }); + ipcRenderer.on('vscode:enterFullScreen', () => setFullscreen(true, mainWindow)); + ipcRenderer.on('vscode:leaveFullScreen', () => setFullscreen(false, mainWindow)); // Proxy Login Dialog ipcRenderer.on('vscode:openProxyAuthenticationDialog', async (event: unknown, payload: { authInfo: AuthInfo; username?: string; password?: string; replyChannel: string }) => { diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index 730c5d7af6b..41d6b89e733 100644 --- a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -20,7 +20,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Barrier } from 'vs/base/common/async'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { applyZoom } from 'vs/platform/window/electron-sandbox/window'; -import { getZoomLevel, isFullscreen } from 'vs/base/browser/browser'; +import { getZoomLevel, isFullscreen, setFullscreen } from 'vs/base/browser/browser'; import { getActiveWindow } from 'vs/base/browser/dom'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { isMacintosh } from 'vs/base/common/platform'; @@ -53,6 +53,8 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { // transitions (Windows, Linux) via window buttons. this.handleMaximizedState(); } + + this.handleFullScreenState(); } private handleMaximizedState(): void { @@ -73,6 +75,13 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { })); } + private async handleFullScreenState(): Promise { + const fullscreen = await this.nativeHostService.isFullScreen({ targetWindowId: this.window.vscodeWindowId }); + if (fullscreen) { + setFullscreen(true, this.window); + } + } + protected override async handleVetoBeforeClose(e: BeforeUnloadEvent, veto: string): Promise { this.preventUnload(e); diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index af155c4ccfc..6a7acef2ae6 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -11,7 +11,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDimension } from 'vs/editor/common/core/dimension'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyValue, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { IGroupModelChangeEvent } from 'vs/workbench/common/editor/editorGroupModel'; import { IRectangle } from 'vs/platform/window/common/window'; @@ -491,6 +491,24 @@ export interface IEditorWorkingSet { readonly name: string; } +export interface IEditorGroupContextKeyProvider { + + /** + * The context key that needs to be set for each editor group context and the global context. + */ + readonly contextKey: RawContextKey; + + /** + * Retrieves the context key value for the given editor group. + */ + readonly getGroupContextKeyValue: (group: IEditorGroup) => T; + + /** + * An event that is fired when there was a change leading to the context key value to be re-evaluated. + */ + readonly onDidChange?: Event; +} + /** * The main service to interact with editor groups across all opened editor parts. */ @@ -561,6 +579,14 @@ export interface IEditorGroupsService extends IEditorGroupsContainer { * Deletes a working set. */ deleteWorkingSet(workingSet: IEditorWorkingSet): void; + + /** + * Registers a context key provider. This provider sets a context key for each scoped editor group context and the global context. + * + * @param provider - The context key provider to be registered. + * @returns - A disposable object to unregister the provider. + */ + registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): IDisposable; } export const enum OpenEditorContext { @@ -649,6 +675,12 @@ export interface IEditorGroup { */ readonly activeEditor: EditorInput | null; + /** + * All selected editor in this group in sequential order. + * The active editor is always part of the selection. + */ + readonly selectedEditors: EditorInput[]; + /** * The editor in the group that is in preview mode if any. There can * only ever be one editor in preview mode. @@ -767,6 +799,20 @@ export interface IEditorGroup { */ isActive(editor: EditorInput | IUntypedEditorInput): boolean; + /** + * Whether the editor is selected in the group. + */ + isSelected(editor: EditorInput): boolean; + + /** + * Set a new selection for this group. This will replace the current + * selection with the new selection. + * + * @param activeSelectedEditor the editor to set as active selected editor + * @param inactiveSelectedEditors the inactive editors to set as selected + */ + setSelection(activeSelectedEditor: EditorInput, inactiveSelectedEditors: EditorInput[]): Promise; + /** * Find out if a certain editor is included in the group. * 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 5d60cb8afdd..80455d79ac1 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, createEditorPart, ITestInstantiationService, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices'; -import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; +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'; import { URI } from 'vs/base/common/uri'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -19,6 +19,9 @@ import { IGroupModelChangeEvent, IGroupEditorMoveEvent, IGroupEditorOpenEvent } import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { Emitter } from 'vs/base/common/event'; +import { isEqual } from 'vs/base/common/resources'; suite('EditorGroupsService', () => { @@ -42,14 +45,19 @@ suite('EditorGroupsService', () => { disposables.clear(); }); - async function createPart(instantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[TestEditorPart, TestInstantiationService]> { + async function createParts(instantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[TestEditorParts, TestInstantiationService]> { instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorFactory).start(accessor)); - const part = await createEditorPart(instantiationService, disposables); - instantiationService.stub(IEditorGroupsService, part); + const parts = await createEditorParts(instantiationService, disposables); + instantiationService.stub(IEditorGroupsService, parts); testLocalInstantiationService = instantiationService; - return [part, instantiationService]; + return [parts, instantiationService]; + } + + async function createPart(instantiationService?: TestInstantiationService): Promise<[TestEditorPart, TestInstantiationService]> { + const [parts, testInstantiationService] = await createParts(instantiationService); + return [parts.testMainPart, testInstantiationService]; } function createTestFileEditorInput(resource: URI, typeId: string): TestFileEditorInput { @@ -1538,6 +1546,64 @@ suite('EditorGroupsService', () => { assert.strictEqual(group.getIndexOfEditor(inputSticky), 0); }); + test('selection: setSelection, isSelected, selectedEditors', async () => { + const [part] = await createPart(); + const group = part.activeGroup; + + const input1 = createTestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + + function isSelection(inputs: TestFileEditorInput[]): boolean { + for (const input of inputs) { + if (group.selectedEditors.indexOf(input) === -1) { + return false; + } + } + return inputs.length === group.selectedEditors.length; + } + + // Active: input1, Selected: input1 + await group.openEditors([input1, input2, input3].map(editor => ({ editor, options: { pinned: true } }))); + + assert.strictEqual(group.isActive(input1), true); + assert.strictEqual(group.isSelected(input1), true); + assert.strictEqual(group.isSelected(input2), false); + assert.strictEqual(group.isSelected(input3), false); + + assert.strictEqual(isSelection([input1]), true); + + // Active: input1, Selected: input1, input3 + await group.setSelection(input1, [input3]); + + assert.strictEqual(group.isActive(input1), true); + assert.strictEqual(group.isSelected(input1), true); + assert.strictEqual(group.isSelected(input2), false); + assert.strictEqual(group.isSelected(input3), true); + + assert.strictEqual(isSelection([input1, input3]), true); + + // Active: input2, Selected: input1, input3 + await group.setSelection(input2, [input1, input3]); + + assert.strictEqual(group.isSelected(input1), true); + assert.strictEqual(group.isActive(input2), true); + assert.strictEqual(group.isSelected(input2), true); + assert.strictEqual(group.isSelected(input3), true); + + assert.strictEqual(isSelection([input1, input2, input3]), true); + + await group.setSelection(input1, []); + + // Selected: input3 + assert.strictEqual(group.isActive(input1), true); + assert.strictEqual(group.isSelected(input1), true); + assert.strictEqual(group.isSelected(input2), false); + assert.strictEqual(group.isSelected(input3), false); + + assert.strictEqual(isSelection([input1]), true); + }); + test('moveEditor with context (across groups)', async () => { const [part] = await createPart(); const group = part.activeGroup; @@ -1969,5 +2035,167 @@ suite('EditorGroupsService', () => { assert.strictEqual(part.activeGroup.isEmpty, true); }); + test('context key provider', async function () { + const disposables = new DisposableStore(); + + // Instantiate workbench and setup initial state + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + const rootContextKeyService = instantiationService.get(IContextKeyService); + + const [parts] = await createParts(instantiationService); + + const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + + const group1 = parts.activeGroup; + const group2 = parts.addGroup(group1, GroupDirection.RIGHT); + + await group2.openEditor(input2, { pinned: true }); + await group1.openEditor(input1, { pinned: true }); + + // Create context key provider + const rawContextKey = new RawContextKey('testContextKey', parts.activeGroup.id); + const contextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: rawContextKey, + getGroupContextKeyValue: (group) => group.id + }; + disposables.add(parts.registerContextKeyProvider(contextKeyProvider)); + + // Initial state: group1 is active + assert.strictEqual(parts.activeGroup.id, group1.id); + + let globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + let group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + let group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group1.id); + assert.strictEqual(group1ContextKeyValue, group1.id); + assert.strictEqual(group2ContextKeyValue, group2.id); + + // Make group2 active and ensure both gloabal and local context key values are updated + parts.activateGroup(group2); + + globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group2.id); + assert.strictEqual(group1ContextKeyValue, group1.id); + assert.strictEqual(group2ContextKeyValue, group2.id); + + // Add a new group and ensure both gloabal and local context key values are updated + // Group 3 will be active + const group3 = parts.addGroup(group2, GroupDirection.RIGHT); + await group3.openEditor(input3, { pinned: true }); + + globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + const group3ContextKeyValue = group3.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group3.id); + assert.strictEqual(group1ContextKeyValue, group1.id); + assert.strictEqual(group2ContextKeyValue, group2.id); + assert.strictEqual(group3ContextKeyValue, group3.id); + + disposables.dispose(); + }); + + test('context key provider: onDidChange', async function () { + const disposables = new DisposableStore(); + + // Instantiate workbench and setup initial state + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + const rootContextKeyService = instantiationService.get(IContextKeyService); + + const parts = await createEditorParts(instantiationService, disposables); + + const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + + const group1 = parts.activeGroup; + const group2 = parts.addGroup(group1, GroupDirection.RIGHT); + + await group2.openEditor(input2, { pinned: true }); + await group1.openEditor(input1, { pinned: true }); + + // Create context key provider + let offset = 0; + const _onDidChange = new Emitter(); + + const rawContextKey = new RawContextKey('testContextKey', parts.activeGroup.id); + const contextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: rawContextKey, + getGroupContextKeyValue: (group) => group.id + offset, + onDidChange: _onDidChange.event + }; + disposables.add(parts.registerContextKeyProvider(contextKeyProvider)); + + // Initial state: group1 is active + assert.strictEqual(parts.activeGroup.id, group1.id); + + let globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + let group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + let group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group1.id + offset); + assert.strictEqual(group1ContextKeyValue, group1.id + offset); + assert.strictEqual(group2ContextKeyValue, group2.id + offset); + + // Make a change to the context key provider and fire onDidChange such that all context key values are updated + offset = 10; + _onDidChange.fire(); + + globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, group1.id + offset); + assert.strictEqual(group1ContextKeyValue, group1.id + offset); + assert.strictEqual(group2ContextKeyValue, group2.id + offset); + + disposables.dispose(); + }); + + test('context key provider: active editor change', async function () { + const disposables = new DisposableStore(); + + // Instantiate workbench and setup initial state + const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables); + const rootContextKeyService = instantiationService.get(IContextKeyService); + + const parts = await createEditorParts(instantiationService, disposables); + + const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + + const group1 = parts.activeGroup; + + await group1.openEditor(input2, { pinned: true }); + await group1.openEditor(input1, { pinned: true }); + + // Create context key provider + const rawContextKey = new RawContextKey('testContextKey', input1.resource.toString()); + const contextKeyProvider: IEditorGroupContextKeyProvider = { + contextKey: rawContextKey, + getGroupContextKeyValue: (group) => group.activeEditor?.resource?.toString() ?? '', + }; + disposables.add(parts.registerContextKeyProvider(contextKeyProvider)); + + // Initial state: input1 is active + assert.strictEqual(isEqual(group1.activeEditor?.resource, input1.resource), true); + + let globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + let group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, input1.resource.toString()); + assert.strictEqual(group1ContextKeyValue, input1.resource.toString()); + + // Make input2 active and ensure both gloabal and local context key values are updated + await group1.openEditor(input2); + + globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key); + group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key); + assert.strictEqual(globalContextKeyValue, input2.resource.toString()); + assert.strictEqual(group1ContextKeyValue, input2.resource.toString()); + + disposables.dispose(); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index ff7ca909d1c..ce1d79f7751 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -39,6 +39,7 @@ export interface INativeWorkbenchEnvironmentService extends IBrowserWorkbenchEnv readonly os: IOSConfiguration; readonly machineId: string; readonly sqmId: string; + readonly devDeviceId: string; // --- Paths readonly execPath: string; @@ -63,6 +64,9 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment @memoize get sqmId() { return this.configuration.sqmId; } + @memoize + get devDeviceId() { return this.configuration.devDeviceId; } + @memoize get remoteAuthority() { return this.configuration.remoteAuthority; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 0feee51ce96..3c7751e1f6b 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -68,6 +68,7 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten onDidEnableExtensions: Event; getExtensions(locations: URI[]): Promise; + getInstalledWorkspaceExtensionLocations(): URI[]; getInstalledWorkspaceExtensions(includeInvalid: boolean): Promise; canInstall(extension: IGalleryExtension | IResourceExtension): Promise; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 9b961506462..7e4bc409689 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,8 +5,9 @@ import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo, - IProductVersion + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, InstallExtensionInfo, + IProductVersion, + ExtensionInstallSource } from 'vs/platform/extensionManagement/common/extensionManagement'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IResourceExtension, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -447,6 +448,10 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return result; } + getInstalledWorkspaceExtensionLocations(): URI[] { + return this.workspaceExtensionManagementService.getInstalledWorkspaceExtensionsLocations(); + } + async getInstalledWorkspaceExtensions(includeInvalid: boolean): Promise { return this.workspaceExtensionManagementService.getInstalled(includeInvalid); } @@ -566,7 +571,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench throw error; } - if (!installOptions?.context?.[EXTENSION_INSTALL_SYNC_CONTEXT]) { + if (installOptions?.context?.[EXTENSION_INSTALL_SOURCE_CONTEXT] !== ExtensionInstallSource.SETTINGS_SYNC) { await this.checkForWorkspaceTrust(manifest, false); } @@ -836,7 +841,7 @@ class WorkspaceExtensionsManagementService extends Disposable { } private async initialize(): Promise { - const existingLocations = this.getWorkspaceExtensionsLocations(); + const existingLocations = this.getInstalledWorkspaceExtensionsLocations(); if (!existingLocations.length) { return; } @@ -942,7 +947,7 @@ class WorkspaceExtensionsManagementService extends Disposable { }>('workspaceextension:uninstall'); } - private getWorkspaceExtensionsLocations(): URI[] { + getInstalledWorkspaceExtensionsLocations(): URI[] { const locations: URI[] = []; try { const parsed = JSON.parse(this.storageService.get(WorkspaceExtensionsManagementService.WORKSPACE_EXTENSIONS_KEY, StorageScope.WORKSPACE, '[]')); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts index 69a430503f9..ccb40a9fbcc 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts @@ -14,10 +14,8 @@ import { NativeRemoteExtensionManagementService } from 'vs/workbench/services/ex import { ILabelService } from 'vs/platform/label/common/label'; import { IExtension } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { NativeExtensionManagementService } from 'vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; export class ExtensionManagementServerService extends Disposable implements IExtensionManagementServerService { @@ -31,8 +29,6 @@ export class ExtensionManagementServerService extends Disposable implements IExt @ISharedProcessService sharedProcessService: ISharedProcessService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, @ILabelService labelService: ILabelService, - @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, - @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IInstantiationService instantiationService: IInstantiationService, ) { super(); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 5e78b841010..79cf8b0ad4e 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, ExtensionManagementError, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, ExtensionManagementError, ExtensionManagementErrorCode, EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -61,7 +61,8 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag return this.downloadAndInstall(extension, installOptions || {}); } try { - return await super.installFromGallery(extension, installOptions); + const clientTargetPlatform = await this.localExtensionManagementServer.extensionManagementService.getTargetPlatform(); + return await super.installFromGallery(extension, { ...installOptions, context: { ...installOptions?.context, [EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]: clientTargetPlatform } }); } catch (error) { switch (error.name) { case ExtensionManagementErrorCode.Download: diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 77969d88de9..0c8d2e27317 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -310,6 +310,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, sqmId: this._telemetryService.sqmId, + devDeviceId: this._telemetryService.devDeviceId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index b145bc80f6c..c44205e2453 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -41,6 +41,7 @@ export interface IExtensionHostInitData { readonly sessionId: string; readonly machineId: string; readonly sqmId: string; + readonly devDeviceId: string; readonly firstSessionDate: string; readonly msftInternal?: boolean; }; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 0cbeb156a4c..28cf88a67b3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -9,12 +9,13 @@ 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', - chatParticipant: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipant.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', @@ -50,7 +51,6 @@ export const allApiProposals = Object.freeze({ 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', - debugFocus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugFocus.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', @@ -76,7 +76,6 @@ export const allApiProposals = Object.freeze({ 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', - languageModels: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageModels.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', @@ -117,7 +116,6 @@ export const allApiProposals = Object.freeze({ 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', - testPreserveFocus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testPreserveFocus.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', diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index 5148918bbbb..617256fb123 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -244,6 +244,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, sqmId: this._telemetryService.sqmId, + devDeviceId: this._telemetryService.devDeviceId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 70910c8ddb9..1c2168068b1 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -19,6 +19,7 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { getErrorMessage } from 'vs/base/common/errors'; import { IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class CachedExtensionScanner { @@ -32,6 +33,7 @@ export class CachedExtensionScanner { @IExtensionsScannerService private readonly _extensionsScannerService: IExtensionsScannerService, @IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService, @IWorkbenchExtensionManagementService private readonly _extensionManagementService: IWorkbenchExtensionManagementService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @ILogService private readonly _logService: ILogService, ) { this.scannedExtensions = new Promise((resolve, reject) => { @@ -60,7 +62,7 @@ export class CachedExtensionScanner { const result = await Promise.allSettled([ this._extensionsScannerService.scanSystemExtensions({ language, useCache: true, checkControlFile: true }), this._extensionsScannerService.scanUserExtensions({ language, profileLocation: this._userDataProfileService.currentProfile.extensionsResource, useCache: true }), - this._extensionManagementService.getInstalledWorkspaceExtensions(false) + this._environmentService.remoteAuthority ? [] : this._extensionManagementService.getInstalledWorkspaceExtensions(false) ]); let scannedSystemExtensions: IScannedExtension[] = [], diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index 49a9a69a671..0074c58d815 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -503,6 +503,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { sessionId: this._telemetryService.sessionId, machineId: this._telemetryService.machineId, sqmId: this._telemetryService.sqmId, + devDeviceId: this._telemetryService.devDeviceId, firstSessionDate: this._telemetryService.firstSessionDate, msftInternal: this._telemetryService.msftInternal }, diff --git a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts index a466cc1f3a1..89e2791637e 100644 --- a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts +++ b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts @@ -15,6 +15,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ 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'; class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService { @@ -25,8 +26,9 @@ class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IRemoteUserDataProfilesService private readonly remoteUserDataProfilesService: IRemoteUserDataProfilesService, + @IActiveLanguagePackService private readonly activeLanguagePackService: IActiveLanguagePackService, + @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, @ILogService private readonly logService: ILogService, - @IActiveLanguagePackService private readonly activeLanguagePackService: IActiveLanguagePackService ) { } whenExtensionsReady(): Promise { @@ -42,7 +44,13 @@ 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', [platform.language, profileLocation, this.environmentService.extensionDevelopmentLocationURI, languagePack]); + const scannedExtensions = await channel.call('scanExtensions', [ + platform.language, + profileLocation, + this.extensionManagementService.getInstalledWorkspaceExtensionLocations(), + this.environmentService.extensionDevelopmentLocationURI, + languagePack + ]); scannedExtensions.forEach((extension) => { extension.extensionLocation = URI.revive(extension.extensionLocation); }); diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 3b7937e3afc..29418eaed3a 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -29,6 +29,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { get sessionId(): string { return this.impl.sessionId; } get machineId(): string { return this.impl.machineId; } get sqmId(): string { return this.impl.sqmId; } + get devDeviceId(): string { return this.impl.devDeviceId; } get firstSessionDate(): string { return this.impl.firstSessionDate; } get msftInternal(): boolean | undefined { return this.impl.msftInternal; } diff --git a/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts index 18d92118512..6d8ec5b211f 100644 --- a/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/common/workbenchCommonProperties.ts @@ -17,11 +17,12 @@ export function resolveWorkbenchCommonProperties( version: string | undefined, machineId: string, sqmId: string, + devDeviceId: string, isInternalTelemetry: boolean, process: INodeProcess, remoteAuthority?: string ): ICommonProperties { - const result = resolveCommonProperties(release, hostname, process.arch, commit, version, machineId, sqmId, isInternalTelemetry); + const result = resolveCommonProperties(release, hostname, process.arch, commit, version, machineId, sqmId, devDeviceId, isInternalTelemetry); const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION)!; const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.APPLICATION)!; diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index f87e5a9f950..384c53bba07 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -28,6 +28,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { get sessionId(): string { return this.impl.sessionId; } get machineId(): string { return this.impl.machineId; } get sqmId(): string { return this.impl.sqmId; } + get devDeviceId(): string { return this.impl.devDeviceId; } get firstSessionDate(): string { return this.impl.firstSessionDate; } get msftInternal(): boolean | undefined { return this.impl.msftInternal; } @@ -45,7 +46,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { const channel = sharedProcessService.getChannel('telemetryAppender'); const config: ITelemetryServiceConfig = { appenders: [new TelemetryAppenderClient(channel)], - commonProperties: resolveWorkbenchCommonProperties(storageService, environmentService.os.release, environmentService.os.hostname, productService.commit, productService.version, environmentService.machineId, environmentService.sqmId, isInternal, process, environmentService.remoteAuthority), + commonProperties: resolveWorkbenchCommonProperties(storageService, environmentService.os.release, environmentService.os.hostname, productService.commit, productService.version, environmentService.machineId, environmentService.sqmId, environmentService.devDeviceId, isInternal, process, environmentService.remoteAuthority), piiPaths: getPiiPathsFromEnvironment(environmentService), sendErrorTelemetry: true }; 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 5f47fd16202..e51736b4ee9 100644 --- a/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts @@ -26,7 +26,7 @@ suite('Telemetry - common properties', function () { }); test('default', function () { - const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', false, process); + const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'somedevDeviceId', false, process); assert.ok('commitHash' in props); assert.ok('sessionID' in props); assert.ok('timestamp' in props); @@ -50,14 +50,14 @@ suite('Telemetry - common properties', function () { testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.APPLICATION, StorageTarget.MACHINE); - const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', false, process); + const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'somedevDeviceId', false, process); assert.ok('common.lastSessionDate' in props); // conditional, see below assert.ok('common.isNewSession' in props); assert.strictEqual(props['common.isNewSession'], '0'); }); test('values chance on ask', async function () { - const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', false, process); + const props = resolveWorkbenchCommonProperties(testStorageService, release(), hostname(), commit, version, 'someMachineId', 'someSqmId', 'somedevDeviceId', false, process); let value1 = props['common.sequence']; let value2 = props['common.sequence']; assert.ok(value1 !== value2, 'seq'); diff --git a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts index 850b58e1e6c..3695379f0e9 100644 --- a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts @@ -5,7 +5,7 @@ import { importAMDNodeModule } from 'vs/amdX'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, keepObserved, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorun, keepObserved } from 'vs/base/common/observable'; import { countEOL } from 'vs/editor/common/core/eolCounter'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Range } from 'vs/editor/common/core/range'; @@ -15,6 +15,7 @@ import { TokenizationStateStore } from 'vs/editor/common/model/textModelTokens'; import { IModelContentChange, IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from 'vs/workbench/services/textMate/browser/arrayOperation'; import type { StateDeltas, TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; import type { applyStateStackDiff, StateStack } from 'vscode-textmate'; @@ -237,13 +238,3 @@ function changesToString(changes: IModelContentChange[]): string { return changes.map(c => Range.lift(c.range).toString() + ' => ' + c.text).join(' & '); } -function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index ec6d4c95742..24f0d458459 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -27,7 +27,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { - private readonly _onDidCreate = this._register(new Emitter()); + private readonly _onDidCreate = this._register(new Emitter({ leakWarningThreshold: 500 /* increased for users with hundreds of inputs opened */ })); readonly onDidCreate = this._onDidCreate.event; private readonly _onDidResolve = this._register(new Emitter()); diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index c498d6c4da4..dd7a8f0d03f 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -437,7 +437,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { } public getThemeSpecificColors(colors: IThemeScopableCustomizations): IThemeScopedCustomizations | undefined { - let themeSpecificColors; + let themeSpecificColors: IThemeScopedCustomizations | undefined; for (const key in colors) { const scopedColors = colors[key]; if (this.isThemeScope(key) && scopedColors instanceof Object && !Array.isArray(scopedColors)) { @@ -446,7 +446,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { const themeId = themeScope.substring(1, themeScope.length - 1); if (this.isThemeScopeMatch(themeId)) { if (!themeSpecificColors) { - themeSpecificColors = {} as IThemeScopedCustomizations; + themeSpecificColors = {}; } const scopedThemeSpecificColors = scopedColors as IThemeScopedCustomizations; for (const subkey in scopedThemeSpecificColors) { diff --git a/src/vs/workbench/services/timer/browser/timerService.ts b/src/vs/workbench/services/timer/browser/timerService.ts index 1c752ee7501..df14ee528cc 100644 --- a/src/vs/workbench/services/timer/browser/timerService.ts +++ b/src/vs/workbench/services/timer/browser/timerService.ts @@ -446,6 +446,12 @@ export interface ITimerService { * @param to to mark name */ getDuration(from: string, to: string): number; + + /** + * Return the timestamp of a mark. + * @param mark mark name + */ + getStartTime(mark: string): number; } export const ITimerService = createDecorator('timerService'); @@ -471,6 +477,11 @@ class PerfMarks { return toEntry.startTime - fromEntry.startTime; } + getStartTime(mark: string): number { + const entry = this._findEntry(mark); + return entry ? entry.startTime : -1; + } + private _findEntry(name: string): perf.PerformanceMark | void { for (const [, marks] of this._entries) { for (let i = marks.length - 1; i >= 0; i--) { @@ -601,6 +612,10 @@ export abstract class AbstractTimerService implements ITimerService { return this._marks.getDuration(from, to); } + getStartTime(mark: string): number { + return this._marks.getStartTime(mark); + } + private _reportStartupTimes(metrics: IStartupMetrics): void { // report IStartupMetrics as telemetry /* __GDPR__ diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts index 5b228f19992..06c54a5729c 100644 --- a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -194,7 +194,7 @@ export class ExtensionsResource implements IProfileResource { async getLocalExtensions(profile: IUserDataProfile): Promise { return this.withProfileScopedServices(profile, async (extensionEnablementService) => { - const result: Array = []; + const result = new Map(); const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); const disabledExtensions = extensionEnablementService.getDisabledExtensions(); for (const extension of installedExtensions) { @@ -210,6 +210,11 @@ export class ExtensionsResource implements IProfileResource { continue; } } + const existing = result.get(identifier.id.toLowerCase()); + if (existing?.disabled) { + // Remove the duplicate disabled extension + result.delete(identifier.id.toLowerCase()); + } const profileExtension: IProfileExtension = { identifier, displayName: extension.manifest.displayName }; if (disabled) { profileExtension.disabled = true; @@ -220,9 +225,9 @@ export class ExtensionsResource implements IProfileResource { if (!profileExtension.version && preRelease) { profileExtension.preRelease = true; } - result.push(profileExtension); + result.set(profileExtension.identifier.id.toLowerCase(), profileExtension); } - return result; + return [...result.values()]; }); } @@ -234,7 +239,7 @@ export class ExtensionsResource implements IProfileResource { return this.userDataProfileStorageService.withProfileScopedStorageService(profile, async storageService => { const disposables = new DisposableStore(); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService])); + const instantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService]))); const extensionEnablementService = disposables.add(instantiationService.createInstance(GlobalExtensionEnablementService)); try { return await fn(extensionEnablementService); diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts index 34e9694bd42..13b592b7e0e 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -10,7 +10,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { INotificationService } from 'vs/platform/notification/common/notification'; import { Emitter, Event } from 'vs/base/common/event'; import * as DOM from 'vs/base/browser/dom'; -import { IUserDataProfileImportExportService, PROFILE_FILTER, PROFILE_EXTENSION, IUserDataProfileContentHandler, IS_PROFILE_IMPORT_IN_PROGRESS_CONTEXT, PROFILES_TITLE, defaultUserDataProfileIcon, IUserDataProfileService, IProfileResourceTreeItem, PROFILES_CATEGORY, IUserDataProfileManagementService, IS_PROFILE_EXPORT_IN_PROGRESS_CONTEXT, ISaveProfileResult, IProfileImportOptions, PROFILE_URL_AUTHORITY, toUserDataProfileUri } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfileImportExportService, PROFILE_FILTER, PROFILE_EXTENSION, IUserDataProfileContentHandler, IS_PROFILE_IMPORT_IN_PROGRESS_CONTEXT, PROFILES_TITLE, defaultUserDataProfileIcon, IUserDataProfileService, IProfileResourceTreeItem, PROFILES_CATEGORY, IUserDataProfileManagementService, IS_PROFILE_EXPORT_IN_PROGRESS_CONTEXT, ISaveProfileResult, IProfileImportOptions, PROFILE_URL_AUTHORITY, toUserDataProfileUri, IUserDataProfileCreateOptions } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IDialogService, IFileDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -19,7 +19,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptorService, IViewsRegistry, TreeItemCollapsibleState, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, ProfileResourceType, UseDefaultProfileFlags, isUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, ProfileResourceType, ProfileResourceTypeFlags, UseDefaultProfileFlags, isUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -550,12 +550,12 @@ export class UserDataProfileImportExportService extends Disposable implements IU } const disposables = new DisposableStore(); try { - const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, this.userDataProfileService.currentProfile)); + const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, this.userDataProfileService.currentProfile, undefined)); const barrier = new Barrier(); const exportAction = new BarrierAction(barrier, new Action('export', localize('export', "Export"), undefined, true, async () => { exportAction.enabled = false; try { - await this.doExportProfile(userDataProfilesExportState); + await this.doExportProfile(userDataProfilesExportState, EXPORT_PROFILE_PREVIEW_VIEW); } catch (error) { exportAction.enabled = true; this.notificationService.error(error); @@ -572,8 +572,18 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - private async createFromProfile(profile: IUserDataProfile, name: string, options?: IUserDataProfileOptions): Promise { - const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, profile); + async exportProfile2(profile: IUserDataProfile): Promise { + const disposables = new DisposableStore(); + try { + const userDataProfilesExportState = disposables.add(this.instantiationService.createInstance(UserDataProfileExportState, profile, undefined)); + await this.doExportProfile(userDataProfilesExportState, ProgressLocation.Notification); + } finally { + disposables.dispose(); + } + } + + 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); await this.progressService.withProgress({ @@ -584,8 +594,10 @@ export class UserDataProfileImportExportService extends Disposable implements IU 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); if (createdProfile) { - reportProgress(localize('progress extensions', "Applying Extensions...")); - await this.instantiationService.createInstance(ExtensionsResource).copy(profile, createdProfile, false); + 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); @@ -597,7 +609,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } async createTroubleshootProfile(): Promise { - const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, this.userDataProfileService.currentProfile); + const userDataProfilesExportState = this.instantiationService.createInstance(UserDataProfileExportState, this.userDataProfileService.currentProfile, undefined); try { const profileTemplate = await userDataProfilesExportState.getProfileTemplate(localize('troubleshoot issue', "Troubleshoot Issue"), undefined); await this.progressService.withProgress({ @@ -620,7 +632,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - private async doExportProfile(userDataProfilesExportState: UserDataProfileExportState): Promise { + private async doExportProfile(userDataProfilesExportState: UserDataProfileExportState, location: ProgressLocation | string): Promise { const profile = await userDataProfilesExportState.getProfileToExport(); if (!profile) { return; @@ -632,7 +644,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU try { await this.progressService.withProgress({ - location: EXPORT_PROFILE_PREVIEW_VIEW, + location, title: localize('profiles.exporting', "{0}: Exporting...", PROFILES_CATEGORY.value), }, async progress => { const id = await this.pickProfileContentHandler(profile.name); @@ -685,7 +697,7 @@ export class UserDataProfileImportExportService extends Disposable implements IU } } - private async resolveProfileTemplate(uri: URI, options?: IProfileImportOptions): Promise { + async resolveProfileTemplate(uri: URI, options?: IProfileImportOptions): Promise { const profileContent = await this.resolveProfileContent(uri); if (profileContent === null) { return null; @@ -704,6 +716,30 @@ export class UserDataProfileImportExportService extends Disposable implements IU profileTemplate.icon = options.icon; } + if (options?.resourceTypeFlags?.settings === false) { + profileTemplate.settings = undefined; + } + + if (options?.resourceTypeFlags?.keybindings === false) { + profileTemplate.keybindings = undefined; + } + + if (options?.resourceTypeFlags?.snippets === false) { + profileTemplate.snippets = undefined; + } + + if (options?.resourceTypeFlags?.tasks === false) { + profileTemplate.tasks = undefined; + } + + if (options?.resourceTypeFlags?.globalState === false) { + profileTemplate.globalState = undefined; + } + + if (options?.resourceTypeFlags?.extensions === false) { + profileTemplate.extensions = undefined; + } + return profileTemplate; } @@ -1349,6 +1385,7 @@ class UserDataProfileExportState extends UserDataProfileImportExportState { constructor( readonly profile: IUserDataProfile, + private readonly exportFlags: ProfileResourceTypeFlags | undefined, @IQuickInputService quickInputService: IQuickInputService, @IFileService private readonly fileService: IFileService, @IInstantiationService private readonly instantiationService: IInstantiationService @@ -1364,49 +1401,61 @@ class UserDataProfileExportState extends UserDataProfileImportExportState { const roots: IProfileResourceTreeItem[] = []; const exportPreviewProfle = this.createExportPreviewProfile(this.profile); - const settingsResource = this.instantiationService.createInstance(SettingsResource); - const settingsContent = await settingsResource.getContent(this.profile); - await settingsResource.apply(settingsContent, exportPreviewProfle); - const settingsResourceTreeItem = this.instantiationService.createInstance(SettingsResourceTreeItem, exportPreviewProfle); - if (await settingsResourceTreeItem.hasContent()) { - roots.push(settingsResourceTreeItem); + if (this.exportFlags?.settings ?? true) { + const settingsResource = this.instantiationService.createInstance(SettingsResource); + const settingsContent = await settingsResource.getContent(this.profile); + await settingsResource.apply(settingsContent, exportPreviewProfle); + const settingsResourceTreeItem = this.instantiationService.createInstance(SettingsResourceTreeItem, exportPreviewProfle); + if (await settingsResourceTreeItem.hasContent()) { + roots.push(settingsResourceTreeItem); + } } - const keybindingsResource = this.instantiationService.createInstance(KeybindingsResource); - const keybindingsContent = await keybindingsResource.getContent(this.profile); - await keybindingsResource.apply(keybindingsContent, exportPreviewProfle); - const keybindingsResourceTreeItem = this.instantiationService.createInstance(KeybindingsResourceTreeItem, exportPreviewProfle); - if (await keybindingsResourceTreeItem.hasContent()) { - roots.push(keybindingsResourceTreeItem); + if (this.exportFlags?.keybindings ?? true) { + const keybindingsResource = this.instantiationService.createInstance(KeybindingsResource); + const keybindingsContent = await keybindingsResource.getContent(this.profile); + await keybindingsResource.apply(keybindingsContent, exportPreviewProfle); + const keybindingsResourceTreeItem = this.instantiationService.createInstance(KeybindingsResourceTreeItem, exportPreviewProfle); + if (await keybindingsResourceTreeItem.hasContent()) { + roots.push(keybindingsResourceTreeItem); + } } - const snippetsResource = this.instantiationService.createInstance(SnippetsResource); - const snippetsContent = await snippetsResource.getContent(this.profile); - await snippetsResource.apply(snippetsContent, exportPreviewProfle); - const snippetsResourceTreeItem = this.instantiationService.createInstance(SnippetsResourceTreeItem, exportPreviewProfle); - if (await snippetsResourceTreeItem.hasContent()) { - roots.push(snippetsResourceTreeItem); + if (this.exportFlags?.snippets ?? true) { + const snippetsResource = this.instantiationService.createInstance(SnippetsResource); + const snippetsContent = await snippetsResource.getContent(this.profile); + await snippetsResource.apply(snippetsContent, exportPreviewProfle); + const snippetsResourceTreeItem = this.instantiationService.createInstance(SnippetsResourceTreeItem, exportPreviewProfle); + if (await snippetsResourceTreeItem.hasContent()) { + roots.push(snippetsResourceTreeItem); + } } - const tasksResource = this.instantiationService.createInstance(TasksResource); - const tasksContent = await tasksResource.getContent(this.profile); - await tasksResource.apply(tasksContent, exportPreviewProfle); - const tasksResourceTreeItem = this.instantiationService.createInstance(TasksResourceTreeItem, exportPreviewProfle); - if (await tasksResourceTreeItem.hasContent()) { - roots.push(tasksResourceTreeItem); + if (this.exportFlags?.tasks ?? true) { + const tasksResource = this.instantiationService.createInstance(TasksResource); + const tasksContent = await tasksResource.getContent(this.profile); + await tasksResource.apply(tasksContent, exportPreviewProfle); + const tasksResourceTreeItem = this.instantiationService.createInstance(TasksResourceTreeItem, exportPreviewProfle); + if (await tasksResourceTreeItem.hasContent()) { + roots.push(tasksResourceTreeItem); + } } - const globalStateResource = joinPath(exportPreviewProfle.globalStorageHome, 'globalState.json').with({ scheme: USER_DATA_PROFILE_EXPORT_PREVIEW_SCHEME }); - const globalStateResourceTreeItem = this.instantiationService.createInstance(GlobalStateResourceExportTreeItem, exportPreviewProfle, globalStateResource); - const content = await globalStateResourceTreeItem.getContent(); - if (content) { - await this.fileService.writeFile(globalStateResource, VSBuffer.fromString(JSON.stringify(JSON.parse(content), null, '\t'))); - roots.push(globalStateResourceTreeItem); + if (this.exportFlags?.globalState ?? true) { + const globalStateResource = joinPath(exportPreviewProfle.globalStorageHome, 'globalState.json').with({ scheme: USER_DATA_PROFILE_EXPORT_PREVIEW_SCHEME }); + const globalStateResourceTreeItem = this.instantiationService.createInstance(GlobalStateResourceExportTreeItem, exportPreviewProfle, globalStateResource); + const content = await globalStateResourceTreeItem.getContent(); + if (content) { + await this.fileService.writeFile(globalStateResource, VSBuffer.fromString(JSON.stringify(JSON.parse(content), null, '\t'))); + roots.push(globalStateResourceTreeItem); + } } - const extensionsResourceTreeItem = this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, exportPreviewProfle); - if (await extensionsResourceTreeItem.hasContent()) { - roots.push(extensionsResourceTreeItem); + if (this.exportFlags?.extensions ?? true) { + const extensionsResourceTreeItem = this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, exportPreviewProfle); + if (await extensionsResourceTreeItem.hasContent()) { + roots.push(extensionsResourceTreeItem); + } } previewFileSystemProvider.setReadOnly(true); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index 7f0b3b60418..d2c5cb4487e 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { localize, localize2 } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfileUpdateOptions, ProfileResourceType } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfileUpdateOptions, ProfileResourceType, ProfileResourceTypeFlags } from 'vs/platform/userDataProfile/common/userDataProfile'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -82,6 +82,11 @@ export interface IProfileImportOptions 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 const IUserDataProfileImportExportService = createDecorator('IUserDataProfileImportExportService'); @@ -91,10 +96,13 @@ export interface IUserDataProfileImportExportService { registerProfileContentHandler(id: string, profileContentHandler: IUserDataProfileContentHandler): IDisposable; unregisterProfileContentHandler(id: string): void; + resolveProfileTemplate(uri: URI): Promise; exportProfile(): Promise; + exportProfile2(profile: IUserDataProfile): 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; createTroubleshootProfile(): Promise; setProfile(profile: IUserDataProfileTemplate): Promise; diff --git a/src/vs/workbench/services/views/browser/treeViewsService.ts b/src/vs/workbench/services/views/browser/treeViewsService.ts deleted file mode 100644 index 26e407c6ea0..00000000000 --- a/src/vs/workbench/services/views/browser/treeViewsService.ts +++ /dev/null @@ -1,12 +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 { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ITreeViewsService as ITreeViewsServiceCommon, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; - -export interface ITreeViewsService extends ITreeViewsServiceCommon { } -export const ITreeViewsService = createDecorator('treeViewsService'); -registerSingleton(ITreeViewsService, TreeviewsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/views/common/treeViewsService.ts b/src/vs/workbench/services/views/common/treeViewsService.ts deleted file mode 100644 index 4785e5fd362..00000000000 --- a/src/vs/workbench/services/views/common/treeViewsService.ts +++ /dev/null @@ -1,34 +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 ITreeViewsService { - readonly _serviceBrand: undefined; - - getRenderedTreeElement(node: string): V | undefined; - addRenderedTreeItemElement(node: string, element: V): void; - removeRenderedTreeItemElement(node: string): void; -} - -export class TreeviewsService implements ITreeViewsService { - _serviceBrand: undefined; - private _renderedElements: Map = new Map(); - - getRenderedTreeElement(node: string): V | undefined { - if (this._renderedElements.has(node)) { - return this._renderedElements.get(node); - } - return undefined; - } - - addRenderedTreeItemElement(node: string, element: V): void { - this._renderedElements.set(node, element); - } - - removeRenderedTreeItemElement(node: string): void { - if (this._renderedElements.has(node)) { - this._renderedElements.delete(node); - } - } -} diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index 2fd6bad674e..42b1eb49af3 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -1019,7 +1019,15 @@ export class StoredFileWorkingCopy extend // Delegate to working copy model save method if any if (typeof resolvedFileWorkingCopy.model.save === 'function') { - stat = await resolvedFileWorkingCopy.model.save(writeFileOptions, saveCancellation.token); + try { + stat = await resolvedFileWorkingCopy.model.save(writeFileOptions, saveCancellation.token); + } catch (error) { + if (saveCancellation.token.isCancellationRequested) { + return undefined; // save was cancelled + } + + throw error; + } } // Otherwise ask for a snapshot and save via file services diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts index 9430f116c9e..7c3a9a9af7e 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts @@ -48,6 +48,12 @@ export interface IWorkingCopyHistoryEntry { * Associated source with the history entry. */ source: SaveSource; + + /** + * Optional additional metadata associated with the + * source that can help to describe the source. + */ + sourceDescription: string | undefined; } export interface IWorkingCopyHistoryEntryDescriptor { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts index 56e3fd5e9cd..4e0f74bdc15 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts @@ -28,7 +28,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { ILogService } from 'vs/platform/log/common/log'; import { SaveSource, SaveSourceRegistry } from 'vs/workbench/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { lastOrDefault } from 'vs/base/common/arrays'; +import { distinct, lastOrDefault } from 'vs/base/common/arrays'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; interface ISerializedWorkingCopyHistoryModel { @@ -41,9 +41,9 @@ interface ISerializedWorkingCopyHistoryModelEntry { readonly id: string; readonly timestamp: number; readonly source?: SaveSource; + readonly sourceDescription?: string; } - export interface IWorkingCopyHistoryModelOptions { /** @@ -119,7 +119,7 @@ export class WorkingCopyHistoryModel { return joinPath(historyHome, hash(workingCopyResource.toString()).toString(16)); } - async addEntry(source = WorkingCopyHistoryModel.FILE_SAVED_SOURCE, timestamp = Date.now(), token: CancellationToken): Promise { + async addEntry(source = WorkingCopyHistoryModel.FILE_SAVED_SOURCE, sourceDescription: string | undefined = undefined, timestamp = Date.now(), token: CancellationToken): Promise { let entryToReplace: IWorkingCopyHistoryEntry | undefined = undefined; // Figure out if the last entry should be replaced based @@ -138,12 +138,12 @@ export class WorkingCopyHistoryModel { // Replace lastest entry in history if (entryToReplace) { - entry = await this.doReplaceEntry(entryToReplace, timestamp, token); + entry = await this.doReplaceEntry(entryToReplace, source, sourceDescription, timestamp, token); } // Add entry to history else { - entry = await this.doAddEntry(source, timestamp, token); + entry = await this.doAddEntry(source, sourceDescription, timestamp, token); } // Flush now if configured @@ -154,7 +154,7 @@ export class WorkingCopyHistoryModel { return entry; } - private async doAddEntry(source: SaveSource, timestamp: number, token: CancellationToken): Promise { + private async doAddEntry(source: SaveSource, sourceDescription: string | undefined = undefined, timestamp: number, token: CancellationToken): Promise { const workingCopyResource = assertIsDefined(this.workingCopyResource); const workingCopyName = assertIsDefined(this.workingCopyName); const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); @@ -170,7 +170,8 @@ export class WorkingCopyHistoryModel { workingCopy: { resource: workingCopyResource, name: workingCopyName }, location, timestamp, - source + source, + sourceDescription }; this.entries.push(entry); @@ -183,13 +184,15 @@ export class WorkingCopyHistoryModel { return entry; } - private async doReplaceEntry(entry: IWorkingCopyHistoryEntry, timestamp: number, token: CancellationToken): Promise { + private async doReplaceEntry(entry: IWorkingCopyHistoryEntry, source: SaveSource, sourceDescription: string | undefined = undefined, timestamp: number, token: CancellationToken): Promise { const workingCopyResource = assertIsDefined(this.workingCopyResource); // Perform a fast clone operation with minimal overhead to the existing location await this.fileService.cloneFile(workingCopyResource, entry.location); // Update entry + entry.source = source; + entry.sourceDescription = sourceDescription; entry.timestamp = timestamp; // Update version ID of model to use for storing later @@ -335,7 +338,8 @@ export class WorkingCopyHistoryModel { workingCopy: { resource: workingCopyResource, name: workingCopyName }, location: entryStat.resource, timestamp: entryStat.mtime, - source: WorkingCopyHistoryModel.FILE_SAVED_SOURCE + source: WorkingCopyHistoryModel.FILE_SAVED_SOURCE, + sourceDescription: undefined }); } } @@ -348,7 +352,8 @@ export class WorkingCopyHistoryModel { entries.set(entry.id, { ...existingEntry, timestamp: entry.timestamp, - source: entry.source ?? existingEntry.source + source: entry.source ?? existingEntry.source, + sourceDescription: entry.sourceDescription ?? existingEntry.sourceDescription }); } } @@ -357,31 +362,58 @@ export class WorkingCopyHistoryModel { return entries; } - async moveEntries(targetWorkingCopyResource: URI, source: SaveSource, token: CancellationToken): Promise { + async moveEntries(target: WorkingCopyHistoryModel, source: SaveSource, token: CancellationToken): Promise { + const timestamp = Date.now(); + const sourceDescription = this.labelService.getUriLabel(assertIsDefined(this.workingCopyResource)); - // Ensure model stored so that any pending data is flushed - await this.store(token); + // Move all entries into the target folder so that we preserve + // any existing history entries that might already be present - if (token.isCancellationRequested) { - return undefined; - } - - // Rename existing entries folder const sourceHistoryEntriesFolder = assertIsDefined(this.historyEntriesFolder); - const targetHistoryFolder = this.toHistoryEntriesFolder(this.historyHome, targetWorkingCopyResource); + const targetHistoryEntriesFolder = assertIsDefined(target.historyEntriesFolder); try { - await this.fileService.move(sourceHistoryEntriesFolder, targetHistoryFolder, true); + for (const entry of this.entries) { + await this.fileService.move(entry.location, joinPath(targetHistoryEntriesFolder, entry.id), true); + } + await this.fileService.del(sourceHistoryEntriesFolder, { recursive: true }); } catch (error) { - if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { - this.traceError(error); + if (!this.isFileNotFound(error)) { + try { + // In case of an error (unless not found), fallback to moving the entire folder + await this.fileService.move(sourceHistoryEntriesFolder, targetHistoryEntriesFolder, true); + } catch (error) { + if (!this.isFileNotFound(error)) { + this.traceError(error); + } + } } } + // Merge our entries with target entries before updating associated working copy + const allEntries = distinct([...this.entries, ...target.entries], entry => entry.id).sort((entryA, entryB) => entryA.timestamp - entryB.timestamp); + // Update our associated working copy + const targetWorkingCopyResource = assertIsDefined(target.workingCopyResource); this.setWorkingCopy(targetWorkingCopyResource); + // Restore our entries and ensure correct metadata + const targetWorkingCopyName = assertIsDefined(target.workingCopyName); + for (const entry of allEntries) { + this.entries.push({ + id: entry.id, + location: joinPath(targetHistoryEntriesFolder, entry.id), + source: entry.source, + sourceDescription: entry.sourceDescription, + timestamp: entry.timestamp, + workingCopy: { + resource: targetWorkingCopyResource, + name: targetWorkingCopyName + } + }); + } + // Add entry for the move - await this.addEntry(source, undefined, token); + await this.addEntry(source, sourceDescription, timestamp, token); // Store model again to updated location await this.store(token); @@ -483,6 +515,7 @@ export class WorkingCopyHistoryModel { return { id: entry.id, source: entry.source !== WorkingCopyHistoryModel.FILE_SAVED_SOURCE ? entry.source : undefined, + sourceDescription: entry.sourceDescription, timestamp: entry.timestamp }; }) @@ -498,7 +531,7 @@ export class WorkingCopyHistoryModel { try { serializedModel = JSON.parse((await this.fileService.readFile(historyEntriesListingFile)).value.toString()); } catch (error) { - if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + if (!this.isFileNotFound(error)) { this.traceError(error); } } @@ -516,7 +549,7 @@ export class WorkingCopyHistoryModel { try { rawEntries = (await this.fileService.resolve(historyEntriesFolder, { resolveMetadata: true })).children; } catch (error) { - if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + if (!this.isFileNotFound(error)) { this.traceError(error); } } @@ -532,6 +565,10 @@ export class WorkingCopyHistoryModel { ); } + private isFileNotFound(error: unknown): boolean { + return error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + } + private traceError(error: Error): void { this.logService.trace('[Working Copy History Service]', error); } @@ -644,14 +681,15 @@ export abstract class WorkingCopyHistoryService extends Disposable implements IW return resources; } - private async doMoveEntries(model: WorkingCopyHistoryModel, source: SaveSource, sourceWorkingCopyResource: URI, targetWorkingCopyResource: URI): Promise { + private async doMoveEntries(source: WorkingCopyHistoryModel, saveSource: SaveSource, sourceWorkingCopyResource: URI, targetWorkingCopyResource: URI): Promise { // Move to target via model - await model.moveEntries(targetWorkingCopyResource, source, CancellationToken.None); + const target = await this.getModel(targetWorkingCopyResource); + await source.moveEntries(target, saveSource, CancellationToken.None); // Update model in our map this.models.delete(sourceWorkingCopyResource); - this.models.set(targetWorkingCopyResource, model); + this.models.set(targetWorkingCopyResource, source); return targetWorkingCopyResource; } @@ -668,7 +706,7 @@ export abstract class WorkingCopyHistoryService extends Disposable implements IW } // Add to model - return model.addEntry(source, timestamp, token); + return model.addEntry(source, undefined, timestamp, token); } async updateEntry(entry: IWorkingCopyHistoryEntry, properties: { source: SaveSource }, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts index 000801f1726..eb982284efd 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts @@ -350,7 +350,14 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp if (result !== false) { await Promises.settled(workingCopies.map(workingCopy => workingCopy.isModified() ? workingCopy.save(saveOptions) : Promise.resolve(true))); } - }, localize('saveBeforeShutdown', "Saving editors with unsaved changes is taking a bit longer...")); + }, + localize('saveBeforeShutdown', "Saving editors with unsaved changes is taking a bit longer..."), + undefined, + // Do not pick `Dialog` as location for reporting progress if it is likely + // that the save operation will itself open a dialog for asking for the + // location to save to for untitled or scratchpad working copies. + // https://github.com/microsoft/vscode-internalbacklog/issues/4943 + workingCopies.some(workingCopy => workingCopy.capabilities & WorkingCopyCapabilities.Untitled || workingCopy.capabilities & WorkingCopyCapabilities.Scratchpad) ? ProgressLocation.Window : ProgressLocation.Dialog); } private doRevertAllBeforeShutdown(modifiedWorkingCopies: IWorkingCopy[]): Promise { @@ -439,13 +446,13 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp }, localize('discardBackupsBeforeShutdown', "Discarding backups is taking a bit longer...")); } - private withProgressAndCancellation(promiseFactory: (token: CancellationToken) => Promise, title: string, detail?: string): Promise { + private withProgressAndCancellation(promiseFactory: (token: CancellationToken) => Promise, title: string, detail?: string, location = ProgressLocation.Dialog): Promise { const cts = new CancellationTokenSource(); return this.progressService.withProgress({ - location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more changes now (https://github.com/microsoft/vscode/issues/122774) - cancellable: true, // allow to cancel (https://github.com/microsoft/vscode/issues/112278) - delay: 800, // delay so that it only appears when operation takes a long time + location, // by default use a dialog to prevent the user from making any more changes now (https://github.com/microsoft/vscode/issues/122774) + cancellable: true, // allow to cancel (https://github.com/microsoft/vscode/issues/112278) + delay: 800, // delay so that it only appears when operation takes a long time title, detail }, () => raceCancellation(promiseFactory(cts.token), cts.token), () => cts.dispose(true)); 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 45f93ad9b70..d4d63ff6958 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -88,12 +88,21 @@ export class TestStoredFileWorkingCopyModelWithCustomSave extends TestStoredFile saveCounter = 0; throwOnSave = false; + saveOperation: Promise | undefined = undefined; async save(options: IWriteFileOptions, token: CancellationToken): Promise { if (this.throwOnSave) { throw new Error('Fail'); } + if (this.saveOperation) { + await this.saveOperation; + } + + if (token.isCancellationRequested) { + throw new Error('Canceled'); + } + this.saveCounter++; return { @@ -190,6 +199,42 @@ suite('StoredFileWorkingCopy (with custom save)', function () { assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.ERROR), true); }); + test('save cancelled (custom implemented)', async () => { + let savedCounter = 0; + let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; + disposables.add(workingCopy.onDidSave(e => { + savedCounter++; + lastSaveEvent = e; + })); + + let saveErrorCounter = 0; + disposables.add(workingCopy.onDidSaveError(() => { + saveErrorCounter++; + })); + + await workingCopy.resolve(); + let resolve: () => void; + (workingCopy.model as TestStoredFileWorkingCopyModelWithCustomSave).saveOperation = new Promise(r => resolve = r); + + workingCopy.model?.updateContents('first'); + const firstSave = workingCopy.save(); + // cancel the first save by requesting a second while it is still mid operation + workingCopy.model?.updateContents('second'); + const secondSave = workingCopy.save(); + resolve!(); + await firstSave; + await secondSave; + + assert.strictEqual(savedCounter, 1); + assert.strictEqual(saveErrorCounter, 0); + assert.strictEqual(workingCopy.isDirty(), false); + assert.strictEqual(lastSaveEvent!.reason, SaveReason.EXPLICIT); + assert.ok(lastSaveEvent!.stat); + assert.ok(isStoredFileWorkingCopySaveEvent(lastSaveEvent!)); + assert.strictEqual(workingCopy.model?.pushedStackElement, true); + assert.strictEqual((workingCopy.model as TestStoredFileWorkingCopyModelWithCustomSave).saveCounter, 1); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); 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 4cbfe873cae..b4ee3100922 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 @@ -56,6 +56,7 @@ const TestNativeWindowConfiguration: INativeWindowConfiguration = { windowId: 0, machineId: 'testMachineId', sqmId: 'testSqmId', + devDeviceId: 'testdevDeviceId', logLevel: LogLevel.Error, loggers: { global: [], window: [] }, mainPid: 0, 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 ff4f48a4af9..40a3f059f9b 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 @@ -674,12 +674,14 @@ suite('WorkingCopyHistoryService', () => { assert.strictEqual(entries[0].id, entry1.id); assert.strictEqual(entries[0].timestamp, entry1.timestamp); assert.strictEqual(entries[0].source, entry1.source); + assert.ok(!entries[0].sourceDescription); assert.notStrictEqual(entries[0].location, entry1.location); assert.strictEqual(entries[0].workingCopy.resource.toString(), renamedWorkingCopyResource.toString()); assert.strictEqual(entries[1].id, entry2.id); assert.strictEqual(entries[1].timestamp, entry2.timestamp); assert.strictEqual(entries[1].source, entry2.source); + assert.ok(!entries[1].sourceDescription); assert.notStrictEqual(entries[1].location, entry2.location); assert.strictEqual(entries[1].workingCopy.resource.toString(), renamedWorkingCopyResource.toString()); @@ -688,6 +690,10 @@ suite('WorkingCopyHistoryService', () => { assert.strictEqual(entries[2].source, entry3.source); assert.notStrictEqual(entries[2].location, entry3.location); assert.strictEqual(entries[2].workingCopy.resource.toString(), renamedWorkingCopyResource.toString()); + assert.ok(!entries[2].sourceDescription); + + assert.strictEqual(entries[3].source, 'renamed.source' /* for the move */); + assert.ok(entries[3].sourceDescription); // contains the source working copy path const all = await service.getAll(CancellationToken.None); assert.strictEqual(all.length, 1); @@ -774,6 +780,9 @@ suite('WorkingCopyHistoryService', () => { assert.notStrictEqual(entries[2].location, entry3B.location); assert.strictEqual(entries[2].workingCopy.resource.toString(), renamedWorkingCopy2Resource.toString()); + assert.strictEqual(entries[3].source, 'moved.source' /* for the move */); + assert.ok(entries[3].sourceDescription); // contains the source working copy path + const all = await service.getAll(CancellationToken.None); assert.strictEqual(all.length, 2); for (const resource of all) { @@ -783,5 +792,133 @@ suite('WorkingCopyHistoryService', () => { } }); + test('move entries (file rename) - preserves previous entries (no new entries)', async () => { + const workingCopyTarget = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopySource = disposables.add(new TestWorkingCopy(testFile2Path)); + + const entry1 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-source1' }, CancellationToken.None); + const entry2 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-source2' }, CancellationToken.None); + const entry3 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-source3' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopyTarget.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + entries = await service.getEntries(workingCopySource.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + await fileService.move(workingCopySource.resource, workingCopyTarget.resource, true); + + const result = await service.moveEntries(workingCopySource.resource, workingCopyTarget.resource); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].toString(), workingCopyTarget.resource.toString()); + + entries = await service.getEntries(workingCopySource.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + entries = await service.getEntries(workingCopyTarget.resource, CancellationToken.None); + assert.strictEqual(entries.length, 4); + + assert.strictEqual(entries[0].id, entry1.id); + assert.strictEqual(entries[0].timestamp, entry1.timestamp); + assert.strictEqual(entries[0].source, entry1.source); + assert.notStrictEqual(entries[0].location, entry1.location); + assert.strictEqual(entries[0].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[1].id, entry2.id); + assert.strictEqual(entries[1].timestamp, entry2.timestamp); + assert.strictEqual(entries[1].source, entry2.source); + assert.notStrictEqual(entries[1].location, entry2.location); + assert.strictEqual(entries[1].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[2].id, entry3.id); + assert.strictEqual(entries[2].timestamp, entry3.timestamp); + assert.strictEqual(entries[2].source, entry3.source); + assert.notStrictEqual(entries[2].location, entry3.location); + assert.strictEqual(entries[2].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[3].source, 'renamed.source' /* for the move */); + assert.ok(entries[3].sourceDescription); // contains the source working copy path + + const all = await service.getAll(CancellationToken.None); + assert.strictEqual(all.length, 1); + assert.strictEqual(all[0].toString(), workingCopyTarget.resource.toString()); + }); + + test('move entries (file rename) - preserves previous entries (new entries)', async () => { + const workingCopyTarget = disposables.add(new TestWorkingCopy(testFile1Path)); + const workingCopySource = disposables.add(new TestWorkingCopy(testFile2Path)); + + const targetEntry1 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-target1' }, CancellationToken.None); + const targetEntry2 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-target2' }, CancellationToken.None); + const targetEntry3 = await addEntry({ resource: workingCopyTarget.resource, source: 'test-target3' }, CancellationToken.None); + + const sourceEntry1 = await addEntry({ resource: workingCopySource.resource, source: 'test-source1' }, CancellationToken.None); + const sourceEntry2 = await addEntry({ resource: workingCopySource.resource, source: 'test-source2' }, CancellationToken.None); + const sourceEntry3 = await addEntry({ resource: workingCopySource.resource, source: 'test-source3' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopyTarget.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + entries = await service.getEntries(workingCopySource.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + await fileService.move(workingCopySource.resource, workingCopyTarget.resource, true); + + const result = await service.moveEntries(workingCopySource.resource, workingCopyTarget.resource); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].toString(), workingCopyTarget.resource.toString()); + + entries = await service.getEntries(workingCopySource.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + entries = await service.getEntries(workingCopyTarget.resource, CancellationToken.None); + assert.strictEqual(entries.length, 7); + + assert.strictEqual(entries[0].id, targetEntry1.id); + assert.strictEqual(entries[0].timestamp, targetEntry1.timestamp); + assert.strictEqual(entries[0].source, targetEntry1.source); + assert.notStrictEqual(entries[0].location, targetEntry1.location); + assert.strictEqual(entries[0].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[1].id, targetEntry2.id); + assert.strictEqual(entries[1].timestamp, targetEntry2.timestamp); + assert.strictEqual(entries[1].source, targetEntry2.source); + assert.notStrictEqual(entries[1].location, targetEntry2.location); + assert.strictEqual(entries[1].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[2].id, targetEntry3.id); + assert.strictEqual(entries[2].timestamp, targetEntry3.timestamp); + assert.strictEqual(entries[2].source, targetEntry3.source); + assert.notStrictEqual(entries[2].location, targetEntry3.location); + assert.strictEqual(entries[2].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[3].id, sourceEntry1.id); + assert.strictEqual(entries[3].timestamp, sourceEntry1.timestamp); + assert.strictEqual(entries[3].source, sourceEntry1.source); + assert.notStrictEqual(entries[3].location, sourceEntry1.location); + assert.strictEqual(entries[3].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[4].id, sourceEntry2.id); + assert.strictEqual(entries[4].timestamp, sourceEntry2.timestamp); + assert.strictEqual(entries[4].source, sourceEntry2.source); + assert.notStrictEqual(entries[4].location, sourceEntry2.location); + assert.strictEqual(entries[4].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[5].id, sourceEntry3.id); + assert.strictEqual(entries[5].timestamp, sourceEntry3.timestamp); + assert.strictEqual(entries[5].source, sourceEntry3.source); + assert.notStrictEqual(entries[5].location, sourceEntry3.location); + assert.strictEqual(entries[5].workingCopy.resource.toString(), workingCopyTarget.resource.toString()); + + assert.strictEqual(entries[6].source, 'renamed.source' /* for the move */); + assert.ok(entries[6].sourceDescription); // contains the source working copy path + + const all = await service.getAll(CancellationToken.None); + assert.strictEqual(all.length, 1); + assert.strictEqual(all[0].toString(), workingCopyTarget.resource.toString()); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index e045e101f15..cf3df1f56ce 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -41,7 +41,7 @@ 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 { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +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'; import { Range } from 'vs/editor/common/core/range'; @@ -52,7 +52,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/common/decorations'; import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorPart, IAuxiliaryEditorPart, IEditorGroupsContainer, IAuxiliaryEditorPartCreateEvent, IEditorWorkingSet } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorPart, IAuxiliaryEditorPart, IEditorGroupsContainer, IAuxiliaryEditorPartCreateEvent, IEditorWorkingSet, IEditorGroupContextKeyProvider } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, ISaveEditorsOptions, IRevertAllEditorsOptions, PreferredGroup, IEditorsChangeEvent, ISaveEditorsResult } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; @@ -867,6 +867,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { centerLayout(active: boolean): void { } isLayoutCentered(): boolean { return false; } createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable { return Disposable.None; } + registerContextKeyProvider(_provider: IEditorGroupContextKeyProvider): IDisposable { throw new Error('not implemented'); } partOptions!: IEditorPartOptions; enforcePartOptions(options: IEditorPartOptions): IDisposable { return Disposable.None; } @@ -884,6 +885,7 @@ export class TestEditorGroupView implements IEditorGroupView { groupsView: IEditorGroupsView = undefined!; activeEditorPane!: IVisibleEditorPane; activeEditor!: EditorInput; + selectedEditors: EditorInput[] = []; previewEditor!: EditorInput; count!: number; stickyCount!: number; @@ -927,6 +929,8 @@ export class TestEditorGroupView implements IEditorGroupView { isSticky(_editor: EditorInput): boolean { return false; } isTransient(_editor: EditorInput): boolean { return false; } isActive(_editor: EditorInput | IUntypedEditorInput): boolean { return false; } + setSelection(_activeSelectedEditor: EditorInput, _inactiveSelectedEditors: EditorInput[]): Promise { throw new Error('not implemented'); } + isSelected(_editor: EditorInput): boolean { return false; } contains(candidate: EditorInput | IUntypedEditorInput): boolean { return false; } moveEditor(_editor: EditorInput, _target: IEditorGroup, _options?: IEditorOptions): boolean { return true; } moveEditors(_editors: EditorInputWithOptions[], _target: IEditorGroup): boolean { return true; } @@ -1839,28 +1843,33 @@ export class TestEditorPart extends MainEditorPart implements IEditorGroupsServi getWorkingSets(): IEditorWorkingSet[] { throw new Error('Method not implemented.'); } applyWorkingSet(workingSet: IEditorWorkingSet | 'empty'): Promise { throw new Error('Method not implemented.'); } deleteWorkingSet(workingSet: IEditorWorkingSet): Promise { throw new Error('Method not implemented.'); } + + registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): IDisposable { throw new Error('Method not implemented.'); } } -export async function createEditorPart(instantiationService: IInstantiationService, disposables: DisposableStore): Promise { +export class TestEditorParts extends EditorParts { + testMainPart!: TestEditorPart; - class TestEditorParts extends EditorParts { + protected override createMainEditorPart(): MainEditorPart { + this.testMainPart = this.instantiationService.createInstance(TestEditorPart, this); - testMainPart!: TestEditorPart; - - protected override createMainEditorPart(): MainEditorPart { - this.testMainPart = instantiationService.createInstance(TestEditorPart, this); - - return this.testMainPart; - } + return this.testMainPart; } +} - const part = disposables.add(instantiationService.createInstance(TestEditorParts)).testMainPart; +export async function createEditorParts(instantiationService: IInstantiationService, disposables: DisposableStore): Promise { + const parts = instantiationService.createInstance(TestEditorParts); + const part = disposables.add(parts).testMainPart; part.create(document.createElement('div')); part.layout(1080, 800, 0, 0); - await part.whenReady; + await parts.whenReady; - return part; + return parts; +} + +export async function createEditorPart(instantiationService: IInstantiationService, disposables: DisposableStore): Promise { + return (await createEditorParts(instantiationService, disposables)).testMainPart; } export class TestListService implements IListService { @@ -2187,6 +2196,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens toggleAppliationScope(): Promise { throw new Error('Not Supported'); } installExtensionsFromProfile(): Promise { throw new Error('Not Supported'); } whenProfileChanged(from: IUserDataProfile, to: IUserDataProfile): Promise { throw new Error('Not Supported'); } + getInstalledWorkspaceExtensionLocations(): URI[] { throw new Error('Method not implemented.'); } getInstalledWorkspaceExtensions(): Promise { throw new Error('Method not implemented.'); } installResourceExtension(): Promise { throw new Error('Method not implemented.'); } getExtensions(): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index d323ca935ae..4f8d24fd9aa 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -93,6 +93,7 @@ export class TestNativeHostService implements INativeHostService { async toggleFullScreen(): Promise { } async handleTitleDoubleClick(): Promise { } async isMaximized(): Promise { return true; } + async isFullScreen(): Promise { return true; } async maximizeWindow(): Promise { } async unmaximizeWindow(): Promise { } async minimizeWindow(): Promise { } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 5e46124e664..e80d251a7fe 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -7847,6 +7847,13 @@ declare module 'vscode' { * The current `Extension` instance. */ readonly extension: Extension; + + /** + * An object that keeps information about how this extension can use language models. + * + * @see {@link lm.sendChatRequest} + */ + readonly languageModelAccessInformation: LanguageModelAccessInformation; } /** @@ -16195,6 +16202,50 @@ declare module 'vscode' { Dynamic = 2 } + /** + * Represents a thread in a debug session. + */ + export class DebugThread { + /** + * Debug session for thread. + */ + readonly session: DebugSession; + + /** + * ID of the associated thread in the debug protocol. + */ + readonly threadId: number; + + /** + * @hidden + */ + private constructor(session: DebugSession, threadId: number); + } + + /** + * Represents a stack frame in a debug session. + */ + export class DebugStackFrame { + /** + * Debug session for thread. + */ + readonly session: DebugSession; + + /** + * ID of the associated thread in the debug protocol. + */ + readonly threadId: number; + /** + * ID of the stack frame in the debug protocol. + */ + readonly frameId: number; + + /** + * @hidden + */ + private constructor(session: DebugSession, threadId: number, frameId: number); + } + /** * Namespace for debug functionality. */ @@ -16245,6 +16296,19 @@ declare module 'vscode' { */ export const onDidChangeBreakpoints: Event; + /** + * The currently focused thread or stack frame, or `undefined` if no + * thread or stack is focused. A thread can be focused any time there is + * an active debug session, while a stack frame can only be focused when + * a session is paused and the call stack has been retrieved. + */ + export const activeStackItem: DebugThread | DebugStackFrame | undefined; + + /** + * An event which fires when the {@link debug.activeStackItem} has changed. + */ + export const onDidChangeActiveStackItem: Event; + /** * Register a {@link DebugConfigurationProvider debug configuration provider} for a specific debug type. * The optional {@link DebugConfigurationProviderTriggerKind triggerKind} can be used to specify when the `provideDebugConfigurations` method of the provider is triggered. @@ -17400,13 +17464,22 @@ declare module 'vscode' { */ readonly continuous?: boolean; + /** + * Controls how test Test Results view is focused. If true, the editor + * will keep the maintain the user's focus. If false, the editor will + * prefer to move focus into the Test Results view, although + * this may be configured by users. + */ + readonly preserveFocus: boolean; + /** * @param include Array of specific tests to run, or undefined to run all tests * @param exclude An array of tests to exclude from the run. * @param profile The run profile used for this request. * @param continuous Whether to run tests continuously as source changes. + * @param preserveFocus Whether to preserve the user's focus when the run is started */ - constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); + constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean, preserveFocus?: boolean); } /** @@ -18320,6 +18393,853 @@ declare module 'vscode' { */ readonly additionalCommonProperties?: Record; } + + /** + * Represents a user request in chat history. + */ + export class ChatRequestTurn { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequestTurn.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The id of the chat participant to which this request was directed. + */ + readonly participant: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command?: string; + + /** + * The references that were used in this message. + */ + readonly references: ChatPromptReference[]; + + /** + * @hidden + */ + private constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string); + } + + /** + * Represents a chat participant's response in chat history. + */ + export class ChatResponseTurn { + /** + * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. + */ + readonly response: ReadonlyArray; + + /** + * The result that was received from the chat participant. + */ + readonly result: ChatResult; + + /** + * The id of the chat participant that this response came from. + */ + readonly participant: string; + + /** + * The name of the command that this response came from. + */ + readonly command?: string; + + /** + * @hidden + */ + private constructor(response: ReadonlyArray, result: ChatResult, participant: string); + } + + /** + * Extra context passed to a participant. + */ + export interface ChatContext { + /** + * All of the chat messages so far in the current chat session. Currently, only chat messages for the current participant are included. + */ + readonly history: ReadonlyArray; + } + + /** + * Represents an error result from a chat request. + */ + export interface ChatErrorDetails { + /** + * An error message that is shown to the user. + */ + message: string; + + /** + * If set to true, the response will be partly blurred out. + */ + responseIsFiltered?: boolean; + } + + /** + * The result of a chat request. + */ + export interface ChatResult { + /** + * If the request resulted in an error, this property defines the error details. + */ + errorDetails?: ChatErrorDetails; + + /** + * Arbitrary metadata for this result. Can be anything, but must be JSON-stringifyable. + */ + readonly metadata?: { readonly [key: string]: any }; + } + + /** + * Represents the type of user feedback received. + */ + export enum ChatResultFeedbackKind { + /** + * The user marked the result as helpful. + */ + Unhelpful = 0, + + /** + * The user marked the result as unhelpful. + */ + Helpful = 1, + } + + /** + * Represents user feedback for a result. + */ + export interface ChatResultFeedback { + /** + * The ChatResult for which the user is providing feedback. + * This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + */ + readonly result: ChatResult; + + /** + * The kind of feedback that was received. + */ + readonly kind: ChatResultFeedbackKind; + } + + /** + * A followup question suggested by the participant. + */ + export interface ChatFollowup { + /** + * The message to send to the chat. + */ + prompt: string; + + /** + * A title to show the user. The prompt will be shown by default, when this is unspecified. + */ + label?: string; + + /** + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID. + * Followups can only invoke a participant that was contributed by the same extension. + */ + participant?: string; + + /** + * By default, the followup goes to the same participant/command. But this property can be set to invoke a different command. + */ + command?: string; + } + + /** + * Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat. + */ + export interface ChatFollowupProvider { + /** + * Provide followups for the given result. + * @param result This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. + * @param token A cancellation token. + */ + provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; + } + + /** + * A chat request handler is a callback that will be invoked when a request is made to a chat participant. + */ + export type ChatRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; + + /** + * A chat participant can be invoked by the user in a chat session, using the `@` prefix. When it is invoked, it handles the chat request and is solely + * responsible for providing a response to the user. A ChatParticipant is created using {@link chat.createChatParticipant}. + */ + export interface ChatParticipant { + /** + * A unique ID for this participant. + */ + readonly id: string; + + /** + * An icon for the participant shown in UI. + */ + iconPath?: Uri | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } | ThemeIcon; + + /** + * The handler for requests to this participant. + */ + requestHandler: ChatRequestHandler; + + /** + * This provider will be called once after each request to retrieve suggested followup questions. + */ + followupProvider?: ChatFollowupProvider; + + /** + * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes + * a result. + * + * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was + * previously returned from this chat participant. + */ + onDidReceiveFeedback: Event; + + /** + * Dispose this participant and free resources. + */ + dispose(): void; + } + + /** + * A reference to a value that the user added to their chat request. + */ + export interface ChatPromptReference { + /** + * A unique identifier for this kind of reference. + */ + readonly id: string; + + /** + * The start and end index of the reference in the {@link ChatRequest.prompt prompt}. When undefined, the reference was not part of the prompt text. + * + * *Note* that the indices take the leading `#`-character into account which means they can + * used to modify the prompt as-is. + */ + readonly range?: [start: number, end: number]; + + /** + * A description of this value that could be used in an LLM prompt. + */ + readonly modelDescription?: string; + + /** + * The value of this reference. The `string | Uri | Location` types are used today, but this could expand in the future. + */ + readonly value: string | Uri | Location | unknown; + } + + /** + * A request to a chat participant. + */ + export interface ChatRequest { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequest.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command: string | undefined; + + /** + * The list of references and their values that are referenced in the prompt. + * + * *Note* that the prompt contains references as authored and that it is up to the participant + * to further modify the prompt, for instance by inlining reference values or creating links to + * headings which contain the resolved values. References are sorted in reverse by their range + * in the prompt. That means the last reference in the prompt is the first in this list. This simplifies + * string-manipulation of the prompt. + */ + readonly references: readonly ChatPromptReference[]; + } + + /** + * The ChatResponseStream is how a participant is able to return content to the chat view. It provides several methods for streaming different types of content + * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it + * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. + */ + export interface ChatResponseStream { + /** + * Push a markdown part to this stream. Short-hand for + * `push(new ChatResponseMarkdownPart(value))`. + * + * @see {@link ChatResponseStream.push} + * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. + */ + markdown(value: string | MarkdownString): void; + + /** + * Push an anchor part to this stream. Short-hand for + * `push(new ChatResponseAnchorPart(value, title))`. + * An anchor is an inline reference to some type of resource. + * + * @param value A uri, location, or symbol information. + * @param title An optional title that is rendered with value. + */ + anchor(value: Uri | Location, title?: string): void; + + /** + * Push a command button part to this stream. Short-hand for + * `push(new ChatResponseCommandButtonPart(value, title))`. + * + * @param command A Command that will be executed when the button is clicked. + */ + button(command: Command): void; + + /** + * Push a filetree part to this stream. Short-hand for + * `push(new ChatResponseFileTreePart(value))`. + * + * @param value File tree data. + * @param baseUri The base uri to which this file tree is relative. + */ + filetree(value: ChatResponseFileTree[], baseUri: Uri): void; + + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + */ + progress(value: string): void; + + /** + * Push a reference to this stream. Short-hand for + * `push(new ChatResponseReferencePart(value))`. + * + * *Note* that the reference is not rendered inline with the response. + * + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ + reference(value: Uri | Location, iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }): void; + + /** + * Pushes a part to this stream. + * + * @param part A response part, rendered or metadata + */ + push(part: ChatResponsePart): void; + } + + /** + * Represents a part of a chat response that is formatted as Markdown. + */ + export class ChatResponseMarkdownPart { + /** + * A markdown string or a string that should be interpreted as markdown. + */ + value: MarkdownString; + + /** + * Create a new ChatResponseMarkdownPart. + * + * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. + */ + constructor(value: string | MarkdownString); + } + + /** + * Represents a file tree structure in a chat response. + */ + export interface ChatResponseFileTree { + /** + * The name of the file or directory. + */ + name: string; + + /** + * An array of child file trees, if the current file tree is a directory. + */ + children?: ChatResponseFileTree[]; + } + + /** + * Represents a part of a chat response that is a file tree. + */ + export class ChatResponseFileTreePart { + /** + * File tree data. + */ + value: ChatResponseFileTree[]; + + /** + * The base uri to which this file tree is relative + */ + baseUri: Uri; + + /** + * Create a new ChatResponseFileTreePart. + * @param value File tree data. + * @param baseUri The base uri to which this file tree is relative. + */ + constructor(value: ChatResponseFileTree[], baseUri: Uri); + } + + /** + * Represents a part of a chat response that is an anchor, that is rendered as a link to a target. + */ + export class ChatResponseAnchorPart { + /** + * The target of this anchor. + */ + value: Uri | Location; + + /** + * An optional title that is rendered with value. + */ + title?: string; + + /** + * Create a new ChatResponseAnchorPart. + * @param value A uri or location. + * @param title An optional title that is rendered with value. + */ + constructor(value: Uri | Location, title?: string); + } + + /** + * Represents a part of a chat response that is a progress message. + */ + export class ChatResponseProgressPart { + /** + * The progress message + */ + value: string; + + /** + * Create a new ChatResponseProgressPart. + * @param value A progress message + */ + constructor(value: string); + } + + /** + * Represents a part of a chat response that is a reference, rendered separately from the content. + */ + export class ChatResponseReferencePart { + /** + * The reference target. + */ + value: Uri | Location; + + /** + * The icon for the reference. + */ + iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }; + + /** + * Create a new ChatResponseReferencePart. + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ + constructor(value: Uri | Location, iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }); + } + + /** + * Represents a part of a chat response that is a button that executes a command. + */ + export class ChatResponseCommandButtonPart { + /** + * The command that will be executed when the button is clicked. + */ + value: Command; + + /** + * Create a new ChatResponseCommandButtonPart. + * @param value A Command that will be executed when the button is clicked. + */ + constructor(value: Command); + } + + /** + * Represents the different chat response types. + */ + export type ChatResponsePart = ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart + | ChatResponseProgressPart | ChatResponseReferencePart | ChatResponseCommandButtonPart; + + + /** + * Namespace for chat functionality. Users interact with chat participants by sending messages + * to them in the chat view. Chat participants can respond with markdown or other types of content + * via the {@link ChatResponseStream}. + */ + export namespace chat { + /** + * Create a new {@link ChatParticipant chat participant} instance. + * + * @param id A unique identifier for the participant. + * @param handler A request handler for the participant. + * @returns A new chat participant + */ + export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant; + } + + /** + * Represents the role of a chat message. This is either the user or the assistant. + */ + export enum LanguageModelChatMessageRole { + /** + * The user role, e.g the human interacting with a language model. + */ + User = 1, + + /** + * The assistant role, e.g. the language model generating responses. + */ + Assistant = 2 + } + + /** + * Represents a message in a chat. Can assume different roles, like user or assistant. + */ + export class LanguageModelChatMessage { + + /** + * Utility to create a new user message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static User(content: string, name?: string): LanguageModelChatMessage; + + /** + * Utility to create a new assistant message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static Assistant(content: string, name?: string): LanguageModelChatMessage; + + /** + * The role of this message. + */ + role: LanguageModelChatMessageRole; + + /** + * The content of this message. + */ + content: string; + + /** + * The optional name of a user for this message. + */ + name: string | undefined; + + /** + * Create a new user message. + * + * @param role The role of the message. + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + constructor(role: LanguageModelChatMessageRole, content: string, name?: string); + } + + /** + * Represents a language model response. + * + * @see {@link LanguageModelAccess.chatRequest} + */ + export interface LanguageModelChatResponse { + + /** + * An async iterable that is a stream of text chunks forming the overall response. + * + * *Note* that this stream will error when during data receiving an error occurs. Consumers of + * the stream should handle the errors accordingly. + * + * @example + * ```ts + * try { + * // consume stream + * for await (const chunk of response.text) { + * console.log(chunk); + * } + * + * } catch(e) { + * // stream ended with an error + * console.error(e); + * } + * ``` + * + * To cancel the stream, the consumer can {@link CancellationTokenSource.cancel cancel} the token that was used to make the request + * or break from the for-loop. + */ + text: AsyncIterable; + } + + /** + * Represents a language model for making chat requests. + * + * @see {@link lm.selectChatModels} + */ + export interface LanguageModelChat { + + /** + * Human-readable name of the language model. + */ + readonly name: string; + + /** + * Opaque identifier of the language model. + */ + readonly id: string; + + /** + * A well-know identifier of the vendor of the language model, a sample is `copilot`, but + * values are defined by extensions contributing chat models and need to be looked up with them. + */ + readonly vendor: string; + + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + readonly family: string; + + /** + * Opaque version string of the model. This is defined by the extension contributing the language model + * and subject to change. + */ + readonly version: string; + + /** + * The maximum number of tokens that can be sent to the model in a single request. + */ + readonly maxInputTokens: number; + + /** + * Make a chat request using a language model. + * + * *Note* that language model use may be subject to access restrictions and user consent. Calling this function + * for the first time (for a extension) will show a consent dialog to the user and because of that this function + * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} + * to check if they have the necessary permissions to make a request. + * + * This function will return a rejected promise if making a request to the language model is not + * possible. Reasons for this can be: + * + * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} + * - model does not exist anymore, see {@link LanguageModelError.NotFound `NotFound`} + * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} + * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} + * + * @param messages An array of message instances. + * @param options Options that control the request. + * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. + * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. + */ + sendRequest(messages: LanguageModelChatMessage[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + + /** + * Count the number of tokens in a message using the model specific tokenizer-logic. + + * @param text A string or a message instance. + * @param token Optional cancellation token. See {@link CancellationTokenSource} for how to create one. + * @returns A thenable that resolves to the number of tokens. + */ + countTokens(text: string | LanguageModelChatMessage, token?: CancellationToken): Thenable; + } + + /** + * Describes how to select language models for chat requests. + * + * @see {@link lm.selectChatModels} + */ + export interface LanguageModelChatSelector { + + /** + * A vendor of language models. + * @see {@link LanguageModelChat.vendor} + */ + vendor?: string; + + /** + * A family of language models. + * @see {@link LanguageModelChat.family} + */ + family?: string; + + /** + * The version of a language model. + * @see {@link LanguageModelChat.version} + */ + version?: string; + + /** + * The identifier of a language model. + * @see {@link LanguageModelChat.id} + */ + id?: string; + } + + /** + * An error type for language model specific errors. + * + * Consumers of language models should check the code property to determine specific + * failure causes, like `if(someError.code === vscode.LanguageModelError.NotFound.name) {...}` + * for the case of referring to an unknown language model. For unspecified errors the `cause`-property + * will contain the actual error. + */ + export class LanguageModelError extends Error { + + /** + * The requestor does not have permissions to use this + * language model + */ + static NoPermissions(message?: string): LanguageModelError; + + /** + * The requestor is blocked from using this language model. + */ + static Blocked(message?: string): LanguageModelError; + + /** + * The language model does not exist. + */ + static NotFound(message?: string): LanguageModelError; + + /** + * A code that identifies this error. + * + * Possible values are names of errors, like {@linkcode LanguageModelError.NotFound NotFound}, + * or `Unknown` for unspecified errors from the language model itself. In the latter case the + * `cause`-property will contain the actual error. + */ + readonly code: string; + } + + /** + * Options for making a chat request using a language model. + * + * @see {@link LanguageModelChat.sendRequest} + */ + export interface LanguageModelChatRequestOptions { + + /** + * A human-readable message that explains why access to a language model is needed and what feature is enabled by it. + */ + justification?: string; + + /** + * A set of options that control the behavior of the language model. These options are specific to the language model + * and need to be lookup in the respective documentation. + */ + modelOptions?: { [name: string]: any }; + } + + /** + * Namespace for language model related functionality. + */ + export namespace lm { + + /** + * An event that is fired when the set of available chat models changes. + */ + export const onDidChangeChatModels: Event; + + /** + * Select chat models by a {@link LanguageModelChatSelector selector}. This can yield in multiple or no chat models and + * extensions must handle these cases, esp. when no chat model exists, gracefully. + * + * ```ts + * + * const models = await vscode.lm.selectChatModels({family: 'gpt-3.5-turbo'})!; + * if (models.length > 0) { + * const [first] = models; + * const response = await first.sendRequest(...) + * // ... + * } else { + * // NO chat models available + * } + * ``` + * + * *Note* that extensions can hold-on to the results returned by this function and use them later. However, when the + * {@link onDidChangeChatModels}-event is fired the list of chat models might have changed and extensions should re-query. + * + * @param selector A chat model selector. When omitted all chat models are returned. + * @returns An array of chat models, can be empty! + */ + export function selectChatModels(selector?: LanguageModelChatSelector): Thenable; + } + + /** + * Represents extension specific information about the access to language models. + */ + export interface LanguageModelAccessInformation { + + /** + * An event that fires when access information changes. + */ + onDidChange: Event; + + /** + * Checks if a request can be made to a language model. + * + * *Note* that calling this function will not trigger a consent UI but just checks for a persisted state. + * + * @param chat A language model chat object. + * @return `true` if a request can be made, `false` if not, `undefined` if the language + * model does not exist or consent hasn't been asked for. + */ + canSendRequest(chat: LanguageModelChat): boolean | undefined; + } } /** diff --git a/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts b/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts new file mode 100644 index 00000000000..63000738c0f --- /dev/null +++ b/src/vscode-dts/vscode.proposed.attributableCoverage.d.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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. + */ + testItem?: TestItem; + + constructor( + uri: Uri, + statementCoverage: TestCoverageCount, + branchCoverage?: TestCoverageCount, + declarationCoverage?: TestCoverageCount, + testItem?: TestItem, + ); + } +} diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts deleted file mode 100644 index 4abc744d5f1..00000000000 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ /dev/null @@ -1,421 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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' { - - /** - * Represents a user request in chat history. - */ - export class ChatRequestTurn { - /** - * The prompt as entered by the user. - * - * Information about variables used in this request is stored in {@link ChatRequestTurn.variables}. - * - * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} - * are not part of the prompt. - */ - readonly prompt: string; - - /** - * The id of the chat participant to which this request was directed. - */ - readonly participant: string; - - /** - * The name of the {@link ChatCommand command} that was selected for this request. - */ - readonly command?: string; - - /** - * The variables that were used in this message. - * TODO@API rename to `references`? - */ - readonly variables: ChatValueReference[]; - - private constructor(prompt: string, command: string | undefined, variables: ChatValueReference[], participant: string); - } - - /** - * Represents a chat participant's response in chat history. - */ - export class ChatResponseTurn { - /** - * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. - */ - readonly response: ReadonlyArray; - - /** - * The result that was received from the chat participant. - */ - readonly result: ChatResult; - - /** - * The id of the chat participant that this response came from. - */ - readonly participant: string; - - /** - * The name of the command that this response came from. - */ - readonly command?: string; - - private constructor(response: ReadonlyArray, result: ChatResult, participant: string); - } - - export interface ChatContext { - /** - * All of the chat messages so far in the current chat session. - */ - readonly history: ReadonlyArray; - } - - /** - * Represents an error result from a chat request. - */ - export interface ChatErrorDetails { - /** - * An error message that is shown to the user. - */ - message: string; - - /** - * If partial markdown content was sent over the {@link ChatRequestHandler handler}'s response stream before the response terminated, then this flag - * can be set to true and it will be rendered with incomplete markdown features patched up. - * - * For example, if the response terminated after sending part of a triple-backtick code block, then the editor will - * render it as a complete code block. - */ - responseIsIncomplete?: boolean; - - /** - * If set to true, the response will be partly blurred out. - */ - responseIsFiltered?: boolean; - } - - /** - * The result of a chat request. - */ - export interface ChatResult { - /** - * If the request resulted in an error, this property defines the error details. - */ - errorDetails?: ChatErrorDetails; - - /** - * Arbitrary metadata for this result. Can be anything, but must be JSON-stringifyable. - */ - readonly metadata?: { readonly [key: string]: any }; - } - - /** - * Represents the type of user feedback received. - */ - export enum ChatResultFeedbackKind { - /** - * The user marked the result as helpful. - */ - Unhelpful = 0, - - /** - * The user marked the result as unhelpful. - */ - Helpful = 1, - } - - /** - * Represents user feedback for a result. - */ - export interface ChatResultFeedback { - /** - * The ChatResult for which the user is providing feedback. - * This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. - */ - readonly result: ChatResult; - - /** - * The kind of feedback that was received. - */ - readonly kind: ChatResultFeedbackKind; - } - - /** - * A followup question suggested by the participant. - */ - export interface ChatFollowup { - /** - * The message to send to the chat. - */ - prompt: string; - - /** - * A title to show the user. The prompt will be shown by default, when this is unspecified. - */ - label?: string; - - /** - * By default, the followup goes to the same participant/command. But this property can be set to invoke a different participant by ID. - * Followups can only invoke a participant that was contributed by the same extension. - */ - participant?: string; - - /** - * By default, the followup goes to the same participant/command. But this property can be set to invoke a different command. - */ - command?: string; - } - - /** - * Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat. - */ - export interface ChatFollowupProvider { - /** - * Provide followups for the given result. - * @param result This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. - * @param token A cancellation token. - */ - provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; - } - - /** - * A chat request handler is a callback that will be invoked when a request is made to a chat participant. - */ - export type ChatRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; - - /** - * A chat participant can be invoked by the user in a chat session, using the `@` prefix. When it is invoked, it handles the chat request and is solely - * responsible for providing a response to the user. A ChatParticipant is created using {@link chat.createChatParticipant}. - */ - export interface ChatParticipant { - /** - * A unique ID for this participant. - */ - readonly id: string; - - /** - * An icon for the participant shown in UI. - */ - iconPath?: Uri | { - /** - * The icon path for the light theme. - */ - light: Uri; - /** - * The icon path for the dark theme. - */ - dark: Uri; - } | ThemeIcon; - - /** - * The handler for requests to this participant. - */ - requestHandler: ChatRequestHandler; - - /** - * This provider will be called once after each request to retrieve suggested followup questions. - */ - followupProvider?: ChatFollowupProvider; - - /** - * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes - * a result. - * - * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was - * previously returned from this chat participant. - */ - onDidReceiveFeedback: Event; - - /** - * Dispose this participant and free resources - */ - dispose(): void; - } - - export interface ChatValueReference { - /** - * The name of the reference. - * TODO@API How to handle name conflicts? Need id vs name? - */ - readonly name: string; - - /** - * The start and end index of the variable in the {@link ChatRequest.prompt prompt}. - * - * *Note* that the indices take the leading `#`-character into account which means they can - * used to modify the prompt as-is. - */ - readonly range: [start: number, end: number]; - - /** - * The value of this reference. The `string | Uri | Location` types are used today, but this could expand in the future. - */ - readonly value: string | Uri | Location | unknown; - } - - export interface ChatRequest { - /** - * The prompt as entered by the user. - * - * Information about variables used in this request is stored in {@link ChatRequest.variables}. - * - * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} - * are not part of the prompt. - */ - readonly prompt: string; - - /** - * The name of the {@link ChatCommand command} that was selected for this request. - */ - readonly command: string | undefined; - - - /** - * The list of variables and their values that are referenced in the prompt. - * - * *Note* that the prompt contains varibale references as authored and that it is up to the participant - * to further modify the prompt, for instance by inlining variable values or creating links to - * headings which contain the resolved values. Variables are sorted in reverse by their range - * in the prompt. That means the last variable in the prompt is the first in this list. This simplifies - * string-manipulation of the prompt. - */ - readonly variables: readonly ChatValueReference[]; - } - - /** - * The ChatResponseStream is how a participant is able to return content to the chat view. It provides several methods for streaming different types of content - * which will be rendered in an appropriate way in the chat view. A participant can use the helper method for the type of content it wants to return, or it - * can instantiate a {@link ChatResponsePart} and use the generic {@link ChatResponseStream.push} method to return it. - */ - export interface ChatResponseStream { - /** - * Push a markdown part to this stream. Short-hand for - * `push(new ChatResponseMarkdownPart(value))`. - * - * @see {@link ChatResponseStream.push} - * @param value A markdown string or a string that should be interpreted as markdown. The boolean form of {@link MarkdownString.isTrusted} is NOT supported. - * @returns This stream. - */ - markdown(value: string | MarkdownString): ChatResponseStream; - - /** - * Push an anchor part to this stream. Short-hand for - * `push(new ChatResponseAnchorPart(value, title))`. - * An anchor is an inline reference to some type of resource. - * - * @param value A uri or location - * @param title An optional title that is rendered with value - * @returns This stream. - */ - anchor(value: Uri | Location, title?: string): ChatResponseStream; - - /** - * Push a command button part to this stream. Short-hand for - * `push(new ChatResponseCommandButtonPart(value, title))`. - * - * @param command A Command that will be executed when the button is clicked. - * @returns This stream. - */ - button(command: Command): ChatResponseStream; - - /** - * Push a filetree part to this stream. Short-hand for - * `push(new ChatResponseFileTreePart(value))`. - * - * @param value File tree data. - * @param baseUri The base uri to which this file tree is relative to. - * @returns This stream. - */ - filetree(value: ChatResponseFileTree[], baseUri: Uri): ChatResponseStream; - - /** - * Push a progress part to this stream. Short-hand for - * `push(new ChatResponseProgressPart(value))`. - * - * @param value A progress message - * @returns This stream. - */ - progress(value: string): ChatResponseStream; - - /** - * Push a reference to this stream. Short-hand for - * `push(new ChatResponseReferencePart(value))`. - * - * *Note* that the reference is not rendered inline with the response. - * - * @param value A uri or location - * @param iconPath Icon for the reference shown in UI - * @returns This stream. - */ - reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: ThemeIcon | { light: Uri; dark: Uri }): ChatResponseStream; - - /** - * Pushes a part to this stream. - * - * @param part A response part, rendered or metadata - */ - push(part: ChatResponsePart): ChatResponseStream; - } - - export class ChatResponseMarkdownPart { - value: MarkdownString; - - /** - * @param value Note: The boolean form of {@link MarkdownString.isTrusted} is NOT supported. - */ - constructor(value: string | MarkdownString); - } - - export interface ChatResponseFileTree { - name: string; - children?: ChatResponseFileTree[]; - } - - export class ChatResponseFileTreePart { - value: ChatResponseFileTree[]; - baseUri: Uri; - constructor(value: ChatResponseFileTree[], baseUri: Uri); - } - - export class ChatResponseAnchorPart { - value: Uri | Location | SymbolInformation; - title?: string; - constructor(value: Uri | Location | SymbolInformation, title?: string); - } - - export class ChatResponseProgressPart { - value: string; - constructor(value: string); - } - - export class ChatResponseReferencePart { - value: Uri | Location | { variableName: string; value?: Uri | Location }; - iconPath?: ThemeIcon | { light: Uri; dark: Uri }; - constructor(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: ThemeIcon | { light: Uri; dark: Uri }); - } - - export class ChatResponseCommandButtonPart { - value: Command; - constructor(value: Command); - } - - /** - * Represents the different chat response types. - */ - export type ChatResponsePart = ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart - | ChatResponseProgressPart | ChatResponseReferencePart | ChatResponseCommandButtonPart; - - - export namespace chat { - /** - * Create a new {@link ChatParticipant chat participant} instance. - * - * @param id A unique identifier for the participant. - * @param handler A request handler for the participant. - * @returns A new chat participant - */ - export function createChatParticipant(id: string, handler: ChatRequestHandler): ChatParticipant; - } -} diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index f8914d33b94..cd2ec7ba919 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -5,55 +5,8 @@ declare module 'vscode' { - /** - * The location at which the chat is happening. - */ - export enum ChatLocation { - /** - * The chat panel - */ - Panel = 1, - /** - * Terminal inline chat - */ - Terminal = 2, - /** - * Notebook inline chat - */ - Notebook = 3, - /** - * Code editor inline chat - */ - Editor = 4 - } - - export interface ChatRequest { - /** - * The attempt number of the request. The first request has attempt number 0. - */ - readonly attempt: number; - - /** - * If automatic command detection is enabled. - */ - readonly enableCommandDetection: boolean; - - /** - * The location at which the chat is happening. This will always be one of the supported values - */ - readonly location: ChatLocation; - } - export interface ChatParticipant { onDidPerformAction: Event; - supportIssueReporting?: boolean; - } - - export interface ChatErrorDetails { - /** - * If set to true, the message content is completely hidden. Only ChatErrorDetails#message will be shown. - */ - responseIsRedacted?: boolean; } /** @@ -102,16 +55,55 @@ declare module 'vscode' { constructor(uri: Uri, edits: TextEdit | TextEdit[]); } + export class ChatResponseConfirmationPart { + title: string; + message: string; + data: any; + constructor(title: string, message: string, data: any); + } + + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseConfirmationPart; + export class ChatResponseWarningPart { value: MarkdownString; constructor(value: string | MarkdownString); } + export class ChatResponseProgressPart2 extends ChatResponseProgressPart { + value: string; + task?: (progress: Progress) => Thenable; + constructor(value: string, task?: (progress: Progress) => Thenable); + } + export interface ChatResponseStream { - textEdit(target: Uri, edits: TextEdit | TextEdit[]): ChatResponseStream; - markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): ChatResponseStream; - detectedParticipant(participant: string, command?: ChatCommand): ChatResponseStream; - push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart): ChatResponseStream; + + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + * @param task If provided, a task to run while the progress is displayed. When the Thenable resolves, the progress will be marked complete in the UI, and the progress message will be updated to the resolved string if one is specified. + * @returns This stream. + */ + progress(value: string, task?: (progress: Progress) => Thenable): void; + + textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; + markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void; + detectedParticipant(participant: string, command?: ChatCommand): void; + push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart | ChatResponseProgressPart2): void; + + /** + * Show an inline message in the chat view asking the user to confirm an action. + * Multiple confirmations may be shown per response. The UI might show "Accept All" / "Reject All" actions. + * @param title The title of the confirmation entry + * @param message An extra message to display to the user + * @param data An arbitrary JSON-stringifiable object that will be included in the ChatRequest when + * the confirmation is accepted or rejected + * 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; + /** * Push a warning to this stream. Short-hand for * `push(new ChatResponseWarningPart(message))`. @@ -119,8 +111,27 @@ declare module 'vscode' { * @param message A warning message * @returns This stream. */ - warning(message: string | MarkdownString): ChatResponseStream; + warning(message: string | MarkdownString): void; + reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void; + + push(part: ExtendedChatResponsePart): void; + } + + /** + * Does this piggy-back on the existing ChatRequest, or is it a different type of request entirely? + * Does it show up in history? + */ + export interface ChatRequest { + /** + * The `data` for any confirmations that were accepted + */ + acceptedConfirmationData?: any[]; + + /** + * The `data` for any confirmations that were rejected + */ + rejectedConfirmationData?: any[]; } // TODO@API fit this into the stream @@ -140,14 +151,17 @@ declare module 'vscode' { } export class ChatCompletionItem { + id: string; label: string | CompletionItemLabel; values: ChatVariableValue[]; + fullName?: string; + icon?: ThemeIcon; insertText?: string; detail?: string; documentation?: string | MarkdownString; command?: Command; - constructor(label: string | CompletionItemLabel, values: ChatVariableValue[]); + constructor(id: string, label: string | CompletionItemLabel, values: ChatVariableValue[]); } export type ChatExtendedRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; @@ -158,7 +172,13 @@ declare module 'vscode' { */ export function createChatParticipant(id: string, handler: ChatExtendedRequestHandler): ChatParticipant; - export function createDynamicChatParticipant(id: string, name: string, publisherName: string, description: string, handler: ChatExtendedRequestHandler): ChatParticipant; + /** + * Current version of the proposal. Changes whenever backwards-incompatible changes are made. + * If a new feature is added that doesn't break existing code, the version is not incremented. When the extension uses this new feature, it should set its engines.vscode version appropriately. + * But if a change is made to an existing feature that would break existing code, the version should be incremented. + * The chat extension should not activate if it doesn't support the current version. + */ + export const _version: 1 | number; } /* @@ -223,70 +243,10 @@ declare module 'vscode' { readonly action: ChatCopyAction | ChatInsertAction | ChatTerminalAction | ChatCommandAction | ChatFollowupAction | ChatBugReportAction | ChatEditorAction; } - /** - * The detail level of this chat variable value. - */ - export enum ChatVariableLevel { - Short = 1, - Medium = 2, - Full = 3 - } - - export interface ChatVariableValue { + export interface ChatPromptReference { /** - * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. + * TODO Needed for now to drive the variableName-type reference, but probably both of these should go away in the future. */ - level: ChatVariableLevel; - - /** - * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. - */ - value: string | Uri; - - /** - * A description of this value, which could be provided to the LLM as a hint. - */ - description?: string; - } - - export interface ChatVariableResolverResponseStream { - /** - * Push a progress part to this stream. Short-hand for - * `push(new ChatResponseProgressPart(value))`. - * - * @param value - * @returns This stream. - */ - progress(value: string): ChatVariableResolverResponseStream; - - /** - * Push a reference to this stream. Short-hand for - * `push(new ChatResponseReferencePart(value))`. - * - * *Note* that the reference is not rendered inline with the response. - * - * @param value A uri or location - * @returns This stream. - */ - reference(value: Uri | Location): ChatVariableResolverResponseStream; - - /** - * Pushes a part to this stream. - * - * @param part A response part, rendered or metadata - */ - push(part: ChatVariableResolverResponsePart): ChatVariableResolverResponseStream; - } - - export type ChatVariableResolverResponsePart = ChatResponseProgressPart | ChatResponseReferencePart; - - export interface ChatVariableResolver { - /** - * A callback to resolve the value of a chat variable. - * @param name The name of the variable. - * @param context Contextual information about this chat request. - * @param token A cancellation token. - */ - resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult; + readonly name: string; } } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts new file mode 100644 index 00000000000..4e328978a9d --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + + /** + * The location at which the chat is happening. + */ + export enum ChatLocation { + /** + * The chat panel + */ + Panel = 1, + /** + * Terminal inline chat + */ + Terminal = 2, + /** + * Notebook inline chat + */ + Notebook = 3, + /** + * Code editor inline chat + */ + Editor = 4 + } + + export interface ChatRequest { + /** + * The attempt number of the request. The first request has attempt number 0. + */ + readonly attempt: number; + + /** + * If automatic command detection is enabled. + */ + readonly enableCommandDetection: boolean; + + /** + * The location at which the chat is happening. This will always be one of the supported values + */ + readonly location: ChatLocation; + } + + export interface ChatParticipant { + supportIssueReporting?: boolean; + + /** + * Temp, support references that are slow to resolve and should be tools rather than references. + */ + supportsSlowReferences?: boolean; + } + + export interface ChatErrorDetails { + /** + * If set to true, the message content is completely hidden. Only ChatErrorDetails#message will be shown. + */ + responseIsRedacted?: boolean; + } + + export namespace chat { + export function createDynamicChatParticipant(id: string, dynamicProps: DynamicChatParticipantProps, handler: ChatExtendedRequestHandler): ChatParticipant; + } + + /** + * These don't get set on the ChatParticipant after creation, like other props, because they are typically defined in package.json and we want them at the time of creation. + */ + export interface DynamicChatParticipantProps { + name: string; + publisherName: string; + description?: string; + fullName?: string; + } +} diff --git a/src/vscode-dts/vscode.proposed.chatProvider.d.ts b/src/vscode-dts/vscode.proposed.chatProvider.d.ts index 6651c4ff859..7fc7c3e3b97 100644 --- a/src/vscode-dts/vscode.proposed.chatProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatProvider.d.ts @@ -19,24 +19,34 @@ declare module 'vscode' { onDidReceiveLanguageModelResponse2?: Event<{ readonly extensionId: string; readonly participant?: string; readonly tokenCount?: number }>; - provideLanguageModelResponse?(messages: LanguageModelChatMessage2[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; - - provideLanguageModelResponse2(messages: LanguageModelChatMessage[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; + provideLanguageModelResponse(messages: LanguageModelChatMessage[], options: { [name: string]: any }, extensionId: string, progress: Progress, token: CancellationToken): Thenable; provideTokenCount(text: string | LanguageModelChatMessage, token: CancellationToken): Thenable; } export interface ChatResponseProviderMetadata { + + readonly vendor: string; + /** - * The name of the model that is used for this chat access. It is expected that the model name can - * be used to lookup properties like token limits and ChatML support + * Human-readable name of the language model. */ - // TODO@API rename to model - name: string; + readonly name: string; + /** + * Opaque family-name of the language model. Values might be `gpt-3.5-turbo`, `gpt4`, `phi2`, or `llama` + * but they are defined by extensions contributing languages and subject to change. + */ + readonly family: string; - version: string; + /** + * Opaque version string of the model. This is defined by the extension contributing the language model + * and subject to change while the identifier is stable. + */ + readonly version: string; - tokens: number; + readonly maxInputTokens: number; + + readonly maxOutputTokens: number; /** * When present, this gates the use of `requestLanguageModelAccess` behind an authorization flow where @@ -46,6 +56,11 @@ declare module 'vscode' { auth?: true | { label: string }; } + export interface ChatResponseProviderMetadata { + // limit this provider to some extensions + extensions?: string[]; + } + export namespace chat { /** diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts index eb6f0882d63..1b404980e2c 100644 --- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts +++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts @@ -9,11 +9,25 @@ declare module 'vscode' { /** * Register a variable which can be used in a chat request to any participant. + * @param id A unique ID for the variable. * @param name The name of the variable, to be used in the chat input as `#name`. - * @param description A description of the variable for the chat input suggest widget. + * @param userDescription A description of the variable for the chat input suggest widget. + * @param modelDescription A description of the variable for the model. + * @param isSlow Temp, to limit access to '#codebase' which is not a 'reference' and will fit into a tools API later. * @param resolver Will be called to provide the chat variable's value when it is used. + * @param fullName The full name of the variable when selecting context in the picker UI. + * @param icon An icon to display when selecting context in the picker UI. */ - export function registerChatVariableResolver(name: string, description: string, resolver: ChatVariableResolver): Disposable; + 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 { @@ -52,5 +66,71 @@ declare module 'vscode' { * @param token A cancellation token. */ resolve(name: string, context: ChatVariableContext, token: CancellationToken): ProviderResult; + + /** + * A callback to resolve the value of a chat variable. + * @param name The name of the variable. + * @param context Contextual information about this chat request. + * @param token A cancellation token. + */ + resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult; } + + + /** + * The detail level of this chat variable value. + */ + export enum ChatVariableLevel { + Short = 1, + Medium = 2, + Full = 3 + } + + export interface ChatVariableValue { + /** + * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. + */ + level: ChatVariableLevel; + + /** + * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. + */ + value: string | Uri; + + /** + * A description of this value, which could be provided to the LLM as a hint. + */ + description?: string; + } + + export interface ChatVariableResolverResponseStream { + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value + * @returns This stream. + */ + progress(value: string): ChatVariableResolverResponseStream; + + /** + * Push a reference to this stream. Short-hand for + * `push(new ChatResponseReferencePart(value))`. + * + * *Note* that the reference is not rendered inline with the response. + * + * @param value A uri or location + * @returns This stream. + */ + reference(value: Uri | Location): ChatVariableResolverResponseStream; + + /** + * Pushes a part to this stream. + * + * @param part A response part, rendered or metadata + */ + push(part: ChatVariableResolverResponsePart): ChatVariableResolverResponseStream; + } + + export type ChatVariableResolverResponsePart = ChatResponseProgressPart | ChatResponseReferencePart; } diff --git a/src/vscode-dts/vscode.proposed.debugFocus.d.ts b/src/vscode-dts/vscode.proposed.debugFocus.d.ts deleted file mode 100644 index 636cb4745f4..00000000000 --- a/src/vscode-dts/vscode.proposed.debugFocus.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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' { - - // See https://github.com/microsoft/vscode/issues/63943 - - export class DebugThread { - /** - * Create a ThreadFocus - * @param session - * @param threadId - */ - constructor(session: DebugSession, threadId: number); - - /** - * Debug session for thread. - */ - readonly session: DebugSession; - - /** - * ID of the associated thread in the debug protocol. - */ - readonly threadId: number; - } - - export class DebugStackFrame { - /** - * Create a StackFrameFocus - * @param session - * @param threadId - * @param frameId - */ - constructor(session: DebugSession, threadId?: number, frameId?: number); - - /** - * Debug session for thread. - */ - readonly session: DebugSession; - - /** - * Id of the associated thread in the debug protocol. - */ - readonly threadId: number; - /** - * Id of the stack frame in the debug protocol. - */ - readonly frameId: number; - } - - - export namespace debug { - /** - * The currently focused thread or stack frame, or `undefined` if no - * thread or stack is focused. - */ - export const activeStackItem: DebugThread | DebugStackFrame | undefined; - - /** - * An event which fires when the {@link debug.activeStackItem} has changed. - */ - export const onDidChangeActiveStackItem: Event; - } -} diff --git a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts index d600adb0fee..f4261b5b6fb 100644 --- a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts @@ -27,11 +27,6 @@ declare module 'vscode' { */ isDefault?: boolean; - /** - * The full name of this participant. - */ - fullName?: string; - /** * When true, this participant is invoked when the user submits their query using ctrl/cmd+enter * TODO@API name diff --git a/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts b/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts index fc2d55aee53..0661036c505 100644 --- a/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts +++ b/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts @@ -33,9 +33,9 @@ declare module 'vscode' { export interface HoverContext { /** - * Whether to increase or decrease the hover's verbosity + * The delta by which to increase/decrease the hover verbosity level */ - readonly action?: HoverVerbosityAction; + readonly verbosityDelta?: number; /** * The previous hover sent for the same position diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index c25195e5aed..472f013cca3 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -6,9 +6,8 @@ declare module 'vscode' { export namespace interactive { - // current version of the proposal. - export const _version: 1 | number; - + // Can be deleted after another insiders + export const _version: number; export function transferActiveChat(toWorkspace: Uri): void; } } diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts deleted file mode 100644 index be546fe75b1..00000000000 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ /dev/null @@ -1,367 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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' { - - // https://github.com/microsoft/vscode/issues/206265 - - /** - * Represents a language model response. - * - * @see {@link LanguageModelAccess.chatRequest} - */ - export interface LanguageModelChatResponse { - - /** - * An async iterable that is a stream of text chunks forming the overall response. - * - * *Note* that this stream will error when during data receiving an error occurs. Consumers of - * the stream should handle the errors accordingly. - * - * @example - * ```ts - * try { - * // consume stream - * for await (const chunk of response.stream) { - * console.log(chunk); - * } - * - * } catch(e) { - * // stream ended with an error - * console.error(e); - * } - * ``` - */ - stream: AsyncIterable; - } - - //TODO@API give this some structure - // https://github.com/openai/openai-openapi/blob/master/openapi.yaml#L7700, https://platform.openai.com/docs/guides/text-generation/chat-completions-api - // https://github.com/ollama/ollama/blob/main/docs/api.md#response-7 - // https://docs.anthropic.com/claude/reference/messages_post - export interface LanguageModelChatResponse2 { - - message: { - role: LanguageModelChatMessageRole.Assistant; - content: AsyncIterable; - }; - - - } - - /** - * Represents the role of a chat message. This is either the user or the assistant/model. - */ - export enum LanguageModelChatMessageRole { - /** - * The user role, e.g the human interacting with a language model. - */ - User = 1, - - /** - * The assistant role, e.g. the language model generating responses. - */ - Assistant = 2 - } - - // TODO@API name: LanguageModelChatMessage once the deprecated stuff is removed - export class LanguageModelChatMessage2 { - /** - * The role of this message. - */ - role: LanguageModelChatMessageRole; - - /** - * The content of this message. - */ - content: string; - - /** - * The optional name of a user for this message. - */ - name: string | undefined; - - /** - * Create a new user message. - * - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - constructor(role: LanguageModelChatMessageRole, content: string, name?: string); - } - - - /** - * @deprecated - */ - export class LanguageModelChatSystemMessage { - - /** - * The content of this message. - */ - content: string; - - /** - * Create a new system message. - * - * @param content The content of the message. - */ - constructor(content: string); - } - - /** - * @deprecated - */ - export class LanguageModelChatUserMessage { - - /** - * The content of this message. - */ - content: string; - - /** - * The optional name of a user for this message. - */ - name: string | undefined; - - /** - * Create a new user message. - * - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - constructor(content: string, name?: string); - } - - /** - * @deprecated - */ - export class LanguageModelChatAssistantMessage { - - /** - * The content of this message. - */ - content: string; - - /** - * The optional name of a user for this message. - */ - name: string | undefined; - - /** - * Create a new assistant message. - * - * @param content The content of the message. - * @param name The optional name of a user for the message. - */ - constructor(content: string, name?: string); - } - - /** - * Different types of language model messages. - * @deprecated - */ - export type LanguageModelChatMessage = LanguageModelChatSystemMessage | LanguageModelChatUserMessage | LanguageModelChatAssistantMessage; - - /** - * Represents information about a registered language model. - */ - export interface LanguageModelInformation { - /** - * The identifier of the language model. - */ - readonly id: string; - - /** - * The human-readable name of the language model. - */ - readonly name: string; - - /** - * The version of the language model. - */ - // TODO@API drop this for now? - readonly version: string; - - /** - * The number of available tokens that can be used when sending requests - * to the language model. - * - * _Note_ that input- and output-tokens count towards this limit. - * - * @see {@link lm.sendChatRequest} - */ - readonly contextLength: number; - } - - /** - * An event describing the change in the set of available language models. - */ - // TODO@API use LanguageModelInformation instead of string? - export interface LanguageModelChangeEvent { - /** - * Added language models. - */ - readonly added: readonly string[]; - /** - * Removed language models. - */ - readonly removed: readonly string[]; - } - - /** - * An error type for language model specific errors. - * - * Consumers of language models should check the code property to determine specific - * failure causes, like `if(someError.code === vscode.LanguageModelError.NotFound.name) {...}` - * for the case of referring to an unknown language model. For unspecified errors the `cause`-property - * will contain the actual error. - */ - export class LanguageModelError extends Error { - - /** - * The language model does not exist. - */ - static NotFound(message?: string): LanguageModelError; - - /** - * The requestor does not have permissions to use this - * language model - */ - static NoPermissions(message?: string): LanguageModelError; - - /** - * The requestor is blocked from using this language model. - */ - static Blocked(message?: string): LanguageModelError; - - /** - * A code that identifies this error. - * - * Possible values are names of errors, like {@linkcode LanguageModelError.NotFound NotFound}, - * or `Unknown` for unspecified errors from the language model itself. In the latter case the - * `cause`-property will contain the actual error. - */ - readonly code: string; - } - - /** - * Options for making a chat request using a language model. - * - * @see {@link lm.chatRequest} - */ - export interface LanguageModelChatRequestOptions { - - /** - * A human-readable message that explains why access to a language model is needed and what feature is enabled by it. - */ - justification?: string; - - /** - * A set of options that control the behavior of the language model. These options are specific to the language model - * and need to be lookup in the respective documentation. - */ - modelOptions?: { [name: string]: any }; - } - - /** - * Namespace for language model related functionality. - */ - export namespace lm { - - /** - * The identifiers of all language models that are currently available. - */ - export const languageModels: readonly string[]; - - /** - * An event that is fired when the set of available language models changes. - */ - export const onDidChangeLanguageModels: Event; - - /** - * Retrieve information about a language model. - * - * @param languageModel A language model identifier. - * @returns A {@link LanguageModelInformation} instance or `undefined` if the language model does not exist. - */ - export function getLanguageModelInformation(languageModel: string): LanguageModelInformation | undefined; - - /** - * Make a chat request using a language model. - * - * - *Note 1:* language model use may be subject to access restrictions and user consent. Calling this function - * for the first time (for a extension) will show a consent dialog to the user and because of that this function - * must _only be called in response to a user action!_ Extension can use {@link LanguageModelAccessInformation.canSendRequest} - * to check if they have the necessary permissions to make a request. - * - * - *Note 2:* language models are contributed by other extensions and as they evolve and change, - * the set of available language models may change over time. Therefore it is strongly recommend to check - * {@link languageModels} for available values and handle missing language models gracefully. - * - * This function will return a rejected promise if making a request to the language model is not - * possible. Reasons for this can be: - * - * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} - * - model does not exist, see {@link LanguageModelError.NotFound `NotFound`} - * - quota limits exceeded, see {@link LanguageModelError.Blocked `Blocked`} - * - other issues in which case extension must check {@link LanguageModelError.cause `LanguageModelError.cause`} - * - * @param languageModel A language model identifier. - * @param messages An array of message instances. - * @param options Options that control the request. - * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. - * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. - */ - export function sendChatRequest(languageModel: string, messages: (LanguageModelChatMessage | LanguageModelChatMessage2)[], options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; - - /** - * Uses the language model specific tokenzier and computes the length in token of a given message. - * - * *Note* that this function will throw when the language model does not exist. - * - * @param languageModel A language model identifier. - * @param text A string or a message instance. - * @param token Optional cancellation token. - * @returns A thenable that resolves to the length of the message in tokens. - */ - // TODO@API `undefined` when the language model does not support computing token length - // ollama has nothing - // anthropic suggests to count after the fact https://github.com/anthropics/anthropic-tokenizer-typescript?tab=readme-ov-file#anthropic-typescript-tokenizer - export function computeTokenLength(languageModel: string, text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token?: CancellationToken): Thenable; - } - - /** - * Represents extension specific information about the access to language models. - */ - export interface LanguageModelAccessInformation { - - /** - * An event that fires when access information changes. - */ - onDidChange: Event; - - /** - * Checks if a request can be made to a language model. - * - * *Note* that calling this function will not trigger a consent UI but just checks. - * - * @param languageModelId A language model identifier. - * @return `true` if a request can be made, `false` if not, `undefined` if the language - * model does not exist or consent hasn't been asked for. - */ - canSendRequest(languageModelId: string): boolean | undefined; - } - - export interface ExtensionContext { - - /** - * An object that keeps information about how this extension can use language models. - * - * @see {@link lm.sendChatRequest} - */ - readonly languageModelAccessInformation: LanguageModelAccessInformation; - } -} diff --git a/src/vscode-dts/vscode.proposed.speech.d.ts b/src/vscode-dts/vscode.proposed.speech.d.ts index 4e0ad0031ce..c78a32ddfd3 100644 --- a/src/vscode-dts/vscode.proposed.speech.d.ts +++ b/src/vscode-dts/vscode.proposed.speech.d.ts @@ -22,10 +22,14 @@ declare module 'vscode' { readonly text?: string; } - export interface SpeechToTextSession extends Disposable { + export interface SpeechToTextSession { readonly onDidChange: Event; } + export interface TextToSpeechOptions { + readonly language?: string; + } + export enum TextToSpeechStatus { Started = 1, Stopped = 2, @@ -37,7 +41,7 @@ declare module 'vscode' { readonly text?: string; } - export interface TextToSpeechSession extends Disposable { + export interface TextToSpeechSession { readonly onDidChange: Event; synthesize(text: string): void; @@ -53,14 +57,14 @@ declare module 'vscode' { readonly text?: string; } - export interface KeywordRecognitionSession extends Disposable { + export interface KeywordRecognitionSession { readonly onDidChange: Event; } export interface SpeechProvider { - provideSpeechToTextSession(token: CancellationToken, options?: SpeechToTextOptions): SpeechToTextSession; - provideTextToSpeechSession(token: CancellationToken): TextToSpeechSession; - provideKeywordRecognitionSession(token: CancellationToken): KeywordRecognitionSession; + provideSpeechToTextSession(token: CancellationToken, options?: SpeechToTextOptions): ProviderResult; + provideTextToSpeechSession(token: CancellationToken, options?: TextToSpeechOptions): ProviderResult; + provideKeywordRecognitionSession(token: CancellationToken): ProviderResult; } export namespace speech { diff --git a/src/vscode-dts/vscode.proposed.testObserver.d.ts b/src/vscode-dts/vscode.proposed.testObserver.d.ts index d4465affbf2..cb8091c887e 100644 --- a/src/vscode-dts/vscode.proposed.testObserver.d.ts +++ b/src/vscode-dts/vscode.proposed.testObserver.d.ts @@ -15,6 +15,11 @@ declare module 'vscode' { */ export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable; + /** + * Registers a provider that can provide follow-up actions for a test failure. + */ + export function registerTestFollowupProvider(provider: TestFollowupProvider): Disposable; + /** * Returns an observer that watches and can request tests. */ @@ -31,6 +36,10 @@ declare module 'vscode' { export const onDidChangeTestResults: Event; } + export interface TestFollowupProvider { + provideFollowup(result: TestRunResult, test: TestResultSnapshot, taskIndex: number, messageIndex: number, token: CancellationToken): ProviderResult; + } + export interface TestObserver { /** * List of tests returned by test provider for files in the workspace. diff --git a/src/vscode-dts/vscode.proposed.testPreserveFocus.d.ts b/src/vscode-dts/vscode.proposed.testPreserveFocus.d.ts deleted file mode 100644 index 1404b4de096..00000000000 --- a/src/vscode-dts/vscode.proposed.testPreserveFocus.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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' { - - // See https://github.com/microsoft/vscode/issues/209491 - - export class TestRunRequest2 extends TestRunRequest { - /** - * Controls how test Test Results view is focused. If true, the editor - * will keep the maintain the user's focus. If false, the editor will - * prefer to move focus into the Test Results view, although - * this may be configured by users. - */ - readonly preserveFocus: boolean; - - /** - * @param include Array of specific tests to run, or undefined to run all tests - * @param exclude An array of tests to exclude from the run. - * @param profile The run profile used for this request. - * @param continuous Whether to run tests continuously as source changes. - * @param preserveFocus Whether to preserve the user's focus when the run is started - */ - constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], profile?: TestRunProfile, continuous?: boolean); - } -} diff --git a/test/automation/package.json b/test/automation/package.json index 8522561fcac..b9dbbec4bb8 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -27,7 +27,7 @@ "devDependencies": { "@types/mkdirp": "^1.0.1", "@types/ncp": "2.0.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/tmp": "0.2.2", "cpx2": "3.0.0", "npm-run-all": "^4.1.5", diff --git a/test/automation/yarn.lock b/test/automation/yarn.lock index debf88d613c..57b641d2b61 100644 --- a/test/automation/yarn.lock +++ b/test/automation/yarn.lock @@ -21,10 +21,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.1.tgz#3b5c3a26393c19b400844ac422bd0f631a94d69d" integrity sha512-aK9jxMypeSrhiYofWWBf/T7O+KwaiAHzM4sveCdWPn71lzUSMimRnKzhXDKfKwV1kWoBo2P1aGgaIYGLf9/ljw== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/tmp@0.2.2": version "0.2.2" @@ -661,6 +663,11 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +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== + universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" diff --git a/test/integration/browser/package.json b/test/integration/browser/package.json index e39606d605b..e87c8669983 100644 --- a/test/integration/browser/package.json +++ b/test/integration/browser/package.json @@ -8,7 +8,7 @@ }, "devDependencies": { "@types/mkdirp": "^1.0.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", "rimraf": "^2.6.1", diff --git a/test/integration/browser/yarn.lock b/test/integration/browser/yarn.lock index 6ecc33e0c71..591b829f1fd 100644 --- a/test/integration/browser/yarn.lock +++ b/test/integration/browser/yarn.lock @@ -33,10 +33,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/rimraf@^2.0.4": version "2.0.4" @@ -142,6 +144,11 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +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== + vscode-uri@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" diff --git a/test/smoke/package.json b/test/smoke/package.json index 13728583887..f2574aba8e6 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -20,7 +20,7 @@ "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/ncp": "2.0.1", - "@types/node": "18.x", + "@types/node": "20.x", "@types/node-fetch": "^2.5.10", "@types/rimraf": "3.0.2", "npm-run-all": "^4.1.5", diff --git a/test/smoke/yarn.lock b/test/smoke/yarn.lock index 00e5dcd85ab..b2c705923a4 100644 --- a/test/smoke/yarn.lock +++ b/test/smoke/yarn.lock @@ -53,10 +53,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/rimraf@3.0.2": version "3.0.2" @@ -609,6 +611,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +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== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index cfc2a5a9890..09f573713a9 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -41,12 +41,13 @@ const minimist = require('minimist'); * coverage: boolean; * coveragePath: string; * coverageFormats: string | string[]; + * 'per-test-coverage': boolean; * help: boolean; * }} */ const args = minimist(process.argv.slice(2), { string: ['grep', 'run', 'runGlob', 'reporter', 'reporter-options', 'waitServer', 'timeout', 'crash-reporter-directory', 'tfs', 'coveragePath', 'coverageFormats'], - boolean: ['build', 'coverage', 'help', 'dev'], + boolean: ['build', 'coverage', 'help', 'dev', 'per-test-coverage'], alias: { 'grep': ['g', 'f'], 'runGlob': ['glob', 'runGrep'], @@ -68,10 +69,11 @@ Options: --runGlob, --glob, --runGrep only run tests matching --build run with build output (out-build) --coverage generate coverage report +--per-test-coverage generate a per-test V8 coverage report, only valid with the full-json-stream reporter --dev, --dev-tools, --devTools open dev tools, keep window open, reuse app data --reporter the mocha reporter (default: "spec") ---reporter-options the mocha reporter options (default: "") ---waitServer port to connect to and wait before running tests +--reporter-options the mocha reporter options (default: "") +--waitServer port to connect to and wait before running tests --timeout timeout for tests --crash-reporter-directory crash reporter directory --tfs TFS server URL @@ -160,7 +162,7 @@ function deserializeError(err) { class IPCRunner extends events.EventEmitter { - constructor() { + constructor(win) { super(); this.didFail = false; @@ -183,6 +185,34 @@ class IPCRunner extends events.EventEmitter { this.emit('fail', deserializeRunnable(test), deserializeError(err)); }); ipcMain.on('pending', (e, test) => this.emit('pending', deserializeRunnable(test))); + + ipcMain.handle('startCoverage', async () => { + win.webContents.debugger.attach(); + await win.webContents.debugger.sendCommand('Debugger.enable'); + await win.webContents.debugger.sendCommand('Profiler.enable'); + await win.webContents.debugger.sendCommand('Profiler.startPreciseCoverage', { + detailed: true, + allowTriggeredUpdates: false, + }); + }); + + const coverageScriptsReported = new Set(); + ipcMain.handle('snapshotCoverage', async (_, test) => { + const coverage = await win.webContents.debugger.sendCommand('Profiler.takePreciseCoverage'); + await Promise.all(coverage.result.map(async (r) => { + if (!coverageScriptsReported.has(r.scriptId)) { + coverageScriptsReported.add(r.scriptId); + const src = await win.webContents.debugger.sendCommand('Debugger.getScriptSource', { scriptId: r.scriptId }); + r.source = src.scriptSource; + } + })); + + if (!test) { + this.emit('coverage init', coverage); + } else { + this.emit('coverage increment', test, coverage); + } + }); } } @@ -274,7 +304,7 @@ app.on('ready', () => { win.loadURL(url.format({ pathname: path.join(__dirname, 'renderer.html'), protocol: 'file:', slashes: true })); - const runner = new IPCRunner(); + const runner = new IPCRunner(win); createStatsCollector(runner); // Handle renderer crashes, #117068 @@ -285,14 +315,18 @@ app.on('ready', () => { } }); + const reporters = []; + if (args.tfs) { - new mocha.reporters.Spec(runner); - new MochaJUnitReporter(runner, { - reporterOptions: { - testsuitesTitle: `${args.tfs} ${process.platform}`, - mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined - } - }); + reporters.push( + new mocha.reporters.Spec(runner), + new MochaJUnitReporter(runner, { + reporterOptions: { + testsuitesTitle: `${args.tfs} ${process.platform}`, + mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${args.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined + } + }), + ); } else { // mocha patches symbols to use windows escape codes, but it seems like // Electron mangles these in its output. @@ -304,10 +338,13 @@ app.on('ready', () => { }); } - applyReporter(runner, args); + reporters.push(applyReporter(runner, args)); } if (!args.dev) { - ipcMain.on('all done', () => app.exit(runner.didFail ? 1 : 0)); + ipcMain.on('all done', async () => { + await Promise.all(reporters.map(r => r.drain?.())); + app.exit(runner.didFail ? 1 : 0); + }); } }); diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index fd22a6e9512..c9f4dfa1e6a 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -6,6 +6,7 @@ /*eslint-env mocha*/ const fs = require('fs'); +const inspector = require('inspector'); (function () { const originals = {}; @@ -169,9 +170,10 @@ function loadTestModules(opts) { }).then(loadModules); } -let currentTestTitle; +/** @type Mocha.Test */ +let currentTest; -function loadTests(opts) { +async function loadTests(opts) { //#region Unexpected Output @@ -185,6 +187,8 @@ function loadTests(opts) { _allowedTestOutput.push(/Deleting [0-9]+ old snapshots/); } + const perTestCoverage = opts['per-test-coverage'] ? await PerTestCoverage.init() : undefined; + const _allowedTestsWithOutput = new Set([ 'creates a snapshot', // self-testing 'validates a snapshot', // self-testing @@ -195,14 +199,15 @@ function loadTests(opts) { 'issue #149130: vscode freezes because of Bracket Pair Colorization', // https://github.com/microsoft/vscode/issues/192440 'property limits', // https://github.com/microsoft/vscode/issues/192443 'Error events', // https://github.com/microsoft/vscode/issues/192443 - 'fetch returns keybinding with user first if title and id matches' // + 'fetch returns keybinding with user first if title and id matches', // + 'throw ListenerLeakError' ]); 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(currentTestTitle)) { + if (!_allowedTestOutput.some(a => a.test(msg)) && !_allowedTestsWithOutput.has(currentTest.title)) { _testsWithUnexpectedOutput = true; consoleFn.apply(console, arguments); } @@ -256,7 +261,7 @@ function loadTests(opts) { event.preventDefault(); // Do not log to test output, we show an error later when test ends event.stopPropagation(); - if (!_allowedTestsWithUnhandledRejections.has(currentTestTitle)) { + if (!_allowedTestsWithUnhandledRejections.has(currentTest.title)) { onUnexpectedError(event.reason); } }); @@ -275,7 +280,12 @@ function loadTests(opts) { }); }); - teardown(() => { + setup(async () => { + await perTestCoverage?.startTest(); + }); + + teardown(async () => { + await perTestCoverage?.finishTest(currentTest.file, currentTest.fullTitle()); // should not have unexpected output if (_testsWithUnexpectedOutput && !opts.dev) { @@ -410,7 +420,7 @@ function runTests(opts) { }); }); - runner.on('test', test => currentTestTitle = test.title); + runner.on('test', test => currentTest = test); if (opts.dev) { runner.on('fail', (test, err) => { @@ -432,3 +442,21 @@ ipcRenderer.on('run', (e, opts) => { ipcRenderer.send('error', err); }); }); + +class PerTestCoverage { + static async init() { + await ipcRenderer.invoke('startCoverage'); + return new PerTestCoverage(); + } + + async startTest() { + if (!this.didInit) { + this.didInit = true; + await ipcRenderer.invoke('snapshotCoverage'); + } + } + + async finishTest(file, fullTitle) { + await ipcRenderer.invoke('snapshotCoverage', { file, fullTitle }); + } +} diff --git a/test/unit/fullJsonStreamReporter.js b/test/unit/fullJsonStreamReporter.js index 07b2315a004..7b345ea70fb 100644 --- a/test/unit/fullJsonStreamReporter.js +++ b/test/unit/fullJsonStreamReporter.js @@ -26,11 +26,15 @@ module.exports = class FullJsonStreamReporter extends BaseRunner { super(runner, options); const total = runner.total; - runner.once(EVENT_RUN_BEGIN, () => writeEvent(['start', { total }])); - runner.once(EVENT_RUN_END, () => writeEvent(['end', this.stats])); + runner.once(EVENT_RUN_BEGIN, () => this.writeEvent(['start', { total }])); + runner.once(EVENT_RUN_END, () => this.writeEvent(['end', this.stats])); - runner.on(EVENT_TEST_BEGIN, test => writeEvent(['testStart', clean(test)])); - runner.on(EVENT_TEST_PASS, test => writeEvent(['pass', clean(test)])); + // custom coverage events: + runner.on('coverage init', (c) => this.writeEvent(['coverageInit', c])); + runner.on('coverage increment', (context, coverage) => this.writeEvent(['coverageIncrement', { ...context, coverage }])); + + runner.on(EVENT_TEST_BEGIN, test => this.writeEvent(['testStart', clean(test)])); + runner.on(EVENT_TEST_PASS, test => this.writeEvent(['pass', clean(test)])); runner.on(EVENT_TEST_FAIL, (test, err) => { test = clean(test); test.actual = err.actual; @@ -40,14 +44,18 @@ module.exports = class FullJsonStreamReporter extends BaseRunner { test.snapshotPath = err.snapshotPath; test.err = err.message; test.stack = err.stack || null; - writeEvent(['fail', test]); + this.writeEvent(['fail', test]); }); } -}; -function writeEvent(event) { - process.stdout.write(JSON.stringify(event) + '\n'); -} + drain() { + return Promise.resolve(this.lastEvent); + } + + writeEvent(event) { + this.lastEvent = new Promise(r => process.stdout.write(JSON.stringify(event) + '\n', r)); + } +}; const clean = test => ({ title: test.title, diff --git a/yarn.lock b/yarn.lock index 55e72181a08..e8145b51ddd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1253,21 +1253,18 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-4.2.22.tgz#cf488a0f6b4a9c245d09927f4f757ca278b9c8ce" integrity sha512-LXRap3bb4AjtLZ5NOFc4ssVZrQPTgdPcNm++0SEJuJZaOA+xHkojJNYqy33A5q/94BmG5tA6yaMeD4VdCv5aSA== -"@types/node@18.x": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@20.x", "@types/node@^20.9.0": + version "20.11.24" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" + integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + dependencies: + undici-types "~5.26.4" "@types/node@^10.11.7": version "10.12.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.21.tgz#7e8a0c34cf29f4e17a36e9bd0ea72d45ba03908e" integrity sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ== -"@types/node@^18.11.18": - version "18.16.19" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.19.tgz#cb03fca8910fdeb7595b755126a8a78144714eea" - integrity sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA== - "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -1322,9 +1319,9 @@ "@types/node" "*" "@types/vinyl@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.4.tgz#9a7a8071c8d14d3a95d41ebe7135babe4ad5995a" - integrity sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ== + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.12.tgz#17642ca9a8ae10f3db018e9f885da4188db4c6e6" + integrity sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw== dependencies: "@types/expect" "^1.20.4" "@types/node" "*" @@ -1520,6 +1517,14 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" +"@vscode/deviceid@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@vscode/deviceid/-/deviceid-0.1.1.tgz#750e2930a3a8fbf3fd610096a8b915dfdb493c89" + integrity sha512-ErpoMeKKNYAkR1IT3zxB5RtiTqEECdh8fxggupWvzuxpTAX77hwOI2NdJ7um+vupnXRBZVx4ugo0+dVHJWUkag== + dependencies: + fs-extra "^11.2.0" + uuid "^9.0.1" + "@vscode/gulp-electron@^1.36.0": version "1.36.0" resolved "https://registry.yarnpkg.com/@vscode/gulp-electron/-/gulp-electron-1.36.0.tgz#b2895c4bafaa0cf2b13042aa654e9fdd1f3a90cd" @@ -3853,13 +3858,13 @@ 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@28.2.8: - version "28.2.8" - resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.8.tgz#b83d70ca00c0e767f0125fcec85f39aafe39ee4c" - integrity sha512-VgXw2OHqPJkobIC7X9eWh3atptjnELaP+zlbF9Oz00ridlaOWmtLPsp6OaXbLw35URpMr0iYesq8okKp7S0k+g== +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== dependencies: "@electron/get" "^2.0.0" - "@types/node" "^18.11.18" + "@types/node" "^20.9.0" extract-zip "^2.0.1" emoji-regex@^7.0.1: @@ -4762,6 +4767,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.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -6282,6 +6296,15 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jszip@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" @@ -7274,10 +7297,10 @@ node-fetch@^2.6.0, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" -node-gyp-build@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" - integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-gyp-build@4.8.1, node-gyp-build@^4.3.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" + integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== node-html-markdown@^1.3.0: version "1.3.0" @@ -9298,7 +9321,7 @@ 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-cjs@npm: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== @@ -9342,15 +9365,6 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.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" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -9404,7 +9418,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", 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== @@ -9439,13 +9453,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.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" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -10048,10 +10055,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.20240408: - version "5.5.0-dev.20240408" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.0-dev.20240408.tgz#337832c87cf0db5a11f9efcff9c789a982ea77c4" - integrity sha512-WCqFA68PbE0+khOu6x2LPxePy0tKdWuNO2m2K4A/L+OPqua1Qmck9OXUQ/5nUd4B/8UlBuhkhuulQbr2LHO9vA== +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== typical@^4.0.0: version "4.0.0" @@ -10089,6 +10096,11 @@ undertaker@^1.2.1: object.reduce "^1.0.0" undertaker-registry "^1.0.0" +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== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -10127,6 +10139,11 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -10202,6 +10219,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -10586,7 +10608,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-cjs@npm:wrap-ansi@^7.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== @@ -10621,15 +10643,6 @@ 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"