diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 6aec2846e63..d2dc4ec6586 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2018,6 +2018,8 @@ export class CommandCenter { d.originalEndLineNumber === c.originalEndLineNumber && d.modifiedStartLineNumber === c.modifiedStartLineNumber && d.modifiedEndLineNumber === c.modifiedEndLineNumber)); + + this.logger.trace(`[CommandCenter][unstageSelectedRanges] changes: ${JSON.stringify(changes)}`); await this._unstageChanges(textEditor, changes); return; } @@ -2076,9 +2078,7 @@ export class CommandCenter { const originalUri = toGitUri(modifiedUri, 'HEAD'); const originalDocument = await workspace.openTextDocument(originalUri); - - const invertedChanges = changes.map(invertLineChange); - const result = applyLineChanges(originalDocument, modifiedDocument, invertedChanges); + const result = applyLineChanges(originalDocument, modifiedDocument, changes); await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result, modifiedDocument.encoding)); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index c2b24e460ea..f2c8146a407 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1021,7 +1021,7 @@ export class Repository implements Disposable { * Quick diff label */ get label(): string { - return l10n.t('Git local changes (working tree)'); + return l10n.t('Git Local Changes (Working Tree)'); } provideOriginalResource(uri: Uri): Uri | undefined { @@ -2805,8 +2805,7 @@ export class Repository implements Disposable { } export class StagedResourceQuickDiffProvider implements QuickDiffProvider { - readonly visible: boolean = true; - readonly label = l10n.t('Git local changes (index)'); + readonly label = l10n.t('Git Local Changes (Index)'); constructor( private readonly _repository: Repository, diff --git a/extensions/html-language-features/server/src/modes/embeddedSupport.ts b/extensions/html-language-features/server/src/modes/embeddedSupport.ts index a6874e043b4..db378dce848 100644 --- a/extensions/html-language-features/server/src/modes/embeddedSupport.ts +++ b/extensions/html-language-features/server/src/modes/embeddedSupport.ts @@ -56,9 +56,10 @@ export function getDocumentRegions(languageService: LanguageService, document: T } importedScripts.push(value); } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') { - if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(scanner.getTokenText())) { + const token = scanner.getTokenText(); + if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(token) || token === 'module') { languageIdFromType = 'javascript'; - } else if (/["']text\/typescript["']/.test(scanner.getTokenText())) { + } else if (/["']text\/typescript["']/.test(token)) { languageIdFromType = 'typescript'; } else { languageIdFromType = undefined; diff --git a/extensions/prompt-basics/snippets/instructions.code-snippets b/extensions/prompt-basics/snippets/instructions.code-snippets index 2f58714740c..89f3046c80e 100644 --- a/extensions/prompt-basics/snippets/instructions.code-snippets +++ b/extensions/prompt-basics/snippets/instructions.code-snippets @@ -5,9 +5,9 @@ "---", "applyTo: '${1|**,**/*.ts|}'", "---", - "${2:Instructions here...}", + "${2:Coding standards, domain knowledge, and preferences that AI should follow.}", ], - "description": "Instructions", + "description": "Instructions guide and customize Chat behavior by providing context, coding standards, and preferences.", "isFileTemplate": true, } } diff --git a/extensions/prompt-basics/snippets/prompt.code-snippets b/extensions/prompt-basics/snippets/prompt.code-snippets index 99115b3dba8..51e456acc25 100644 --- a/extensions/prompt-basics/snippets/prompt.code-snippets +++ b/extensions/prompt-basics/snippets/prompt.code-snippets @@ -1,13 +1,13 @@ { "fileTemplate": { - "prefix": "New Chat Prompt", + "prefix": "New Reusable Chat Prompt", "body": [ "---", "mode: '${1|ask,edit,agent|}'", "---", - "${2:Prompt description}", + "${2:Expected output and any relevant constraints for this task.}", ], - "description": "Template for chat prompt", + "description": "Prompts define reusable and task-specific steps for automating Chat workflows.", "isFileTemplate": true, } } diff --git a/extensions/typescript-language-features/src/tsconfig.ts b/extensions/typescript-language-features/src/tsconfig.ts index e85c715e875..01a88a43f34 100644 --- a/extensions/typescript-language-features/src/tsconfig.ts +++ b/extensions/typescript-language-features/src/tsconfig.ts @@ -29,7 +29,7 @@ export function inferredProjectCompilerOptions( module: (version.gte(API.v540) ? 'Preserve' : 'ESNext') as Proto.ModuleKind, moduleResolution: (version.gte(API.v540) ? 'Bundler' : 'Node') as Proto.ModuleResolutionKind, target: 'ES2022' as Proto.ScriptTarget, - jsx: 'react' as Proto.JsxEmit, + jsx: 'react-jsx' as Proto.JsxEmit, }; if (version.gte(API.v500)) { diff --git a/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.ts b/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.ts index d7adb1bfc95..3c65b1d661b 100644 --- a/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.ts +++ b/src/vs/editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.ts @@ -7,7 +7,6 @@ import { Range } from '../../../core/range.js'; import { BaseToken } from '../../baseToken.js'; import { Dash } from '../../simpleCodec/tokens/dash.js'; import { NewLine } from '../../linesCodec/tokens/newLine.js'; -import { assert } from '../../../../../base/common/assert.js'; import { MarkdownExtensionsToken } from './markdownExtensionsToken.js'; import { CarriageReturn } from '../../linesCodec/tokens/carriageReturn.js'; @@ -39,13 +38,6 @@ export class FrontMatterMarker extends MarkdownExtensionsToken { range: Range, public readonly tokens: readonly TMarkerToken[], ) { - const lastToken = tokens[tokens.length - 1]; - - assert( - lastToken instanceof NewLine, - `Front Matter marker must end with a new line token, got '${lastToken}'.`, - ); - super(range); } diff --git a/src/vs/editor/common/core/positionToOffset.ts b/src/vs/editor/common/core/positionToOffset.ts index 7fa4e2d59a4..dd8d81a2cbe 100644 --- a/src/vs/editor/common/core/positionToOffset.ts +++ b/src/vs/editor/common/core/positionToOffset.ts @@ -12,16 +12,51 @@ import { Range } from './range.js'; import { SingleTextEdit, TextEdit } from './textEdit.js'; import { TextLength } from './textLength.js'; -export function getPositionOffsetTransformerFromTextModel(textModel: ITextModel): PositionOffsetTransformer { - const text = textModel.getValue(); - return new PositionOffsetTransformer(text); +export abstract class PositionOffsetTransformerBase { + abstract getOffset(position: Position): number; + + getOffsetRange(range: Range): OffsetRange { + return new OffsetRange( + this.getOffset(range.getStartPosition()), + this.getOffset(range.getEndPosition()) + ); + } + + abstract getPosition(offset: number): Position; + + getRange(offsetRange: OffsetRange): Range { + return Range.fromPositions( + this.getPosition(offsetRange.start), + this.getPosition(offsetRange.endExclusive) + ); + } + + getOffsetEdit(edit: TextEdit): OffsetEdit { + const edits = edit.edits.map(e => this.getSingleOffsetEdit(e)); + return new OffsetEdit(edits); + } + + getSingleOffsetEdit(edit: SingleTextEdit): SingleOffsetEdit { + return new SingleOffsetEdit(this.getOffsetRange(edit.range), edit.text); + } + + getSingleTextEdit(edit: SingleOffsetEdit): SingleTextEdit { + return new SingleTextEdit(this.getRange(edit.replaceRange), edit.newText); + } + + getTextEdit(edit: OffsetEdit): TextEdit { + const edits = edit.edits.map(e => this.getSingleTextEdit(e)); + return new TextEdit(edits); + } } -export class PositionOffsetTransformer { +export class PositionOffsetTransformer extends PositionOffsetTransformerBase { private readonly lineStartOffsetByLineIdx: number[]; private readonly lineEndOffsetByLineIdx: number[]; constructor(public readonly text: string) { + super(); + this.lineStartOffsetByLineIdx = []; this.lineEndOffsetByLineIdx = []; @@ -39,7 +74,7 @@ export class PositionOffsetTransformer { this.lineEndOffsetByLineIdx.push(text.length); } - getOffset(position: Position): number { + override getOffset(position: Position): number { const valPos = this._validatePosition(position); return this.lineStartOffsetByLineIdx[valPos.lineNumber - 1] + valPos.column - 1; } @@ -63,27 +98,13 @@ export class PositionOffsetTransformer { return position; } - getOffsetRange(range: Range): OffsetRange { - return new OffsetRange( - this.getOffset(range.getStartPosition()), - this.getOffset(range.getEndPosition()) - ); - } - - getPosition(offset: number): Position { + override getPosition(offset: number): Position { const idx = findLastIdxMonotonous(this.lineStartOffsetByLineIdx, i => i <= offset); const lineNumber = idx + 1; const column = offset - this.lineStartOffsetByLineIdx[idx] + 1; return new Position(lineNumber, column); } - getRange(offsetRange: OffsetRange): Range { - return Range.fromPositions( - this.getPosition(offsetRange.start), - this.getPosition(offsetRange.endExclusive) - ); - } - getTextLength(offsetRange: OffsetRange): TextLength { return TextLength.ofRange(this.getRange(offsetRange)); } @@ -96,22 +117,22 @@ export class PositionOffsetTransformer { getLineLength(lineNumber: number): number { return this.lineEndOffsetByLineIdx[lineNumber - 1] - this.lineStartOffsetByLineIdx[lineNumber - 1]; } +} - getOffsetEdit(edit: TextEdit): OffsetEdit { - const edits = edit.edits.map(e => this.getSingleOffsetEdit(e)); - return new OffsetEdit(edits); +export function getPositionOffsetTransformerFromTextModel(textModel: ITextModel): PositionOffsetTransformerBase { + return new PositionOffsetTransformerWithTextModel(textModel); +} + +class PositionOffsetTransformerWithTextModel extends PositionOffsetTransformerBase { + constructor(private readonly _textModel: ITextModel) { + super(); } - getSingleOffsetEdit(edit: SingleTextEdit): SingleOffsetEdit { - return new SingleOffsetEdit(this.getOffsetRange(edit.range), edit.text); + override getOffset(position: Position): number { + return this._textModel.getOffsetAt(position); } - getSingleTextEdit(edit: SingleOffsetEdit): SingleTextEdit { - return new SingleTextEdit(this.getRange(edit.replaceRange), edit.newText); - } - - getTextEdit(edit: OffsetEdit): TextEdit { - const edits = edit.edits.map(e => this.getSingleTextEdit(e)); - return new TextEdit(edits); + override getPosition(offset: number): Position { + return this._textModel.getPositionAt(offset); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts index adc37495bd8..e930859c2e7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts @@ -12,7 +12,7 @@ import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; import { applyEditsToRanges, OffsetEdit, SingleOffsetEdit } from '../../../../common/core/offsetEdit.js'; import { OffsetRange } from '../../../../common/core/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; -import { getPositionOffsetTransformerFromTextModel, PositionOffsetTransformer } from '../../../../common/core/positionToOffset.js'; +import { getPositionOffsetTransformerFromTextModel, PositionOffsetTransformerBase } from '../../../../common/core/positionToOffset.js'; import { Range } from '../../../../common/core/range.js'; import { SingleTextEdit, StringText, TextEdit } from '../../../../common/core/textEdit.js'; import { TextLength } from '../../../../common/core/textLength.js'; @@ -161,7 +161,7 @@ class InlineSuggestDisplayLocation implements IDisplayLocation { public readonly label: string, ) { } - public withEdit(edit: OffsetEdit, positionOffsetTransformer: PositionOffsetTransformer): InlineSuggestDisplayLocation | undefined { + public withEdit(edit: OffsetEdit, positionOffsetTransformer: PositionOffsetTransformerBase): InlineSuggestDisplayLocation | undefined { const newOffsetRange = applyEditsToRanges([this._offsetRange], edit)[0]; if (!newOffsetRange || newOffsetRange.length !== this._offsetRange.length) { return undefined; diff --git a/src/vs/editor/test/common/utils/testDecoder.ts b/src/vs/editor/test/common/utils/testDecoder.ts index 410606fbffc..28b5130fa26 100644 --- a/src/vs/editor/test/common/utils/testDecoder.ts +++ b/src/vs/editor/test/common/utils/testDecoder.ts @@ -221,7 +221,7 @@ export class TestDecoder> extends assert( receivedToken.equals(expectedToken), - `Expected token '${i}' to be '${expectedToken}', got '${receivedToken}'.`, + `\nExpected token '${i}' to be:\n\n${expectedToken.text}\n(${expectedToken.range})\n\ngot:\n\n${receivedToken.text}\n(${receivedToken.range})\n`, ); } diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 1507e3476ad..06e1a28cf02 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -62,11 +62,14 @@ export abstract class CommontExtensionManagementService extends Disposable imple _serviceBrand: undefined; + readonly preferPreReleases: boolean; + constructor( @IProductService protected readonly productService: IProductService, @IAllowedExtensionsService protected readonly allowedExtensionsService: IAllowedExtensionsService, ) { super(); + this.preferPreReleases = this.productService.quality !== 'stable'; } async canInstall(extension: IGalleryExtension): Promise { @@ -341,9 +344,16 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio this.logService.info('Installing the extension without checking dependencies and pack', task.identifier.id); } else { try { - const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, task.manifest, !!task.options.installPreReleaseVersion, task.options.productVersion); + let preferPreRelease = this.preferPreReleases; + if (task.options.installPreReleaseVersion) { + preferPreRelease = true; + } else if (!URI.isUri(task.source) && task.source.hasPreReleaseVersion) { + // Explicitly asked to install the release version + preferPreRelease = false; + } + const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, task.manifest, preferPreRelease, task.options.productVersion); const installed = await this.getInstalled(undefined, task.options.profileLocation, task.options.productVersion); - const options: InstallExtensionTaskOptions = { ...task.options, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; + const options: InstallExtensionTaskOptions = { ...task.options, pinned: false, installGivenVersion: false, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) { const existing = installed.find(e => areSameExtensions(e.identifier, gallery.identifier)); // Skip if the extension is already installed and has the same application scope @@ -579,7 +589,7 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio throw error; } - private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, installPreRelease: boolean, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { + private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, preferPreRelease: boolean, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { if (!this.galleryService.isEnabled()) { return []; } @@ -603,7 +613,7 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio // filter out known extensions const ids = dependenciesAndPackExtensions.filter(id => knownIdentifiers.every(galleryIdentifier => !areSameExtensions(galleryIdentifier, { id }))); if (ids.length) { - const galleryExtensions = await this.galleryService.getExtensions(ids.map(id => ({ id, preRelease: installPreRelease })), CancellationToken.None); + const galleryExtensions = await this.galleryService.getExtensions(ids.map(id => ({ id, preRelease: preferPreRelease })), CancellationToken.None); for (const galleryExtension of galleryExtensions) { if (knownIdentifiers.find(identifier => areSameExtensions(identifier, galleryExtension.identifier))) { continue; @@ -611,7 +621,7 @@ export abstract class AbstractExtensionManagementService extends CommontExtensio const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier)); let compatible; try { - compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease, productVersion); + compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, preferPreRelease, productVersion); } catch (error) { if (!isDependency) { this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id, getErrorMessage(error)); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index e53143f1b84..109607b695c 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -596,6 +596,8 @@ export const IExtensionManagementService = createDecorator; onDidInstallExtensions: Event; onUninstallExtension: Event; diff --git a/src/vs/platform/markers/common/markerService.ts b/src/vs/platform/markers/common/markerService.ts index 68e6d0391bd..7e5b2d10cd8 100644 --- a/src/vs/platform/markers/common/markerService.ts +++ b/src/vs/platform/markers/common/markerService.ts @@ -11,7 +11,7 @@ import { ResourceMap, ResourceSet } from '../../../base/common/map.js'; import { Schemas } from '../../../base/common/network.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; -import { IMarker, IMarkerData, IMarkerService, IResourceMarker, MarkerSeverity, MarkerStatistics } from './markers.js'; +import { IMarker, IMarkerData, IMarkerReadOptions, IMarkerService, IResourceMarker, MarkerSeverity, MarkerStatistics } from './markers.js'; export const unsupportedSchemas = new Set([ Schemas.inMemory, @@ -326,7 +326,7 @@ export class MarkerService implements IMarkerService { }; } - read(filter: { owner?: string; resource?: URI; severities?: number; take?: number } = Object.create(null)): IMarker[] { + read(filter: IMarkerReadOptions = Object.create(null)): IMarker[] { let { owner, resource, severities, take } = filter; @@ -336,7 +336,7 @@ export class MarkerService implements IMarkerService { if (owner && resource) { // exactly one owner AND resource - const reasons = this._filteredResources.get(resource); + const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined; if (reasons?.length) { const infoMarker = this._createFilteredMarker(resource, reasons); return [infoMarker]; @@ -352,7 +352,7 @@ export class MarkerService implements IMarkerService { if (take > 0 && result.length === take) { break; } - const reasons = this._filteredResources.get(resource); + const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(resource) : undefined; if (reasons?.length) { result.push(this._createFilteredMarker(resource, reasons)); @@ -379,7 +379,7 @@ export class MarkerService implements IMarkerService { if (take > 0 && result.length === take) { break; } - const reasons = this._filteredResources.get(data.resource); + const reasons = !filter.ignoreResourceFilters ? this._filteredResources.get(data.resource) : undefined; if (reasons?.length) { result.push(this._createFilteredMarker(data.resource, reasons)); filtered.add(data.resource); diff --git a/src/vs/platform/markers/common/markers.ts b/src/vs/platform/markers/common/markers.ts index 2c7f1668514..cc686747a83 100644 --- a/src/vs/platform/markers/common/markers.ts +++ b/src/vs/platform/markers/common/markers.ts @@ -10,6 +10,14 @@ import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; +export interface IMarkerReadOptions { + owner?: string; + resource?: URI; + severities?: number; + take?: number; + ignoreResourceFilters?: boolean; +} + export interface IMarkerService { readonly _serviceBrand: undefined; @@ -21,7 +29,7 @@ export interface IMarkerService { remove(owner: string, resources: URI[]): void; - read(filter?: { owner?: string; resource?: URI; severities?: number; take?: number }): IMarker[]; + read(filter?: IMarkerReadOptions): IMarker[]; installResourceFilter(resource: URI, reason: string): IDisposable; diff --git a/src/vs/platform/markers/test/common/markerService.test.ts b/src/vs/platform/markers/test/common/markerService.test.ts index 4ccdd62b50e..adcf3760025 100644 --- a/src/vs/platform/markers/test/common/markerService.test.ts +++ b/src/vs/platform/markers/test/common/markerService.test.ts @@ -243,6 +243,39 @@ suite('Marker Service', () => { assert.strictEqual(service.read({ resource: resource2 }).length, 1); }); + test('resource filter hides markers for the filtered resource UNLESS explicit read', () => { + service = new markerService.MarkerService(); + const resource1 = URI.parse('file:///path/file1.cs'); + const resource2 = URI.parse('file:///path/file2.cs'); + + // Add markers to both resources + service.changeOne('owner1', resource1, [randomMarkerData()]); + service.changeOne('owner1', resource2, [randomMarkerData()]); + + // Verify both resources have markers + assert.strictEqual(service.read().length, 2); + assert.strictEqual(service.read({ resource: resource1 }).length, 1); + assert.strictEqual(service.read({ resource: resource2 }).length, 1); + + // Install filter for resource1 + const filter = service.installResourceFilter(resource1, 'Test filter'); + + // Verify resource1 markers are filtered out, but have 1 info marker instead + assert.strictEqual(service.read().length, 2); // 1 real + 1 info + assert.strictEqual(service.read({ resource: resource1 }).length, 1); // 1 info + assert.strictEqual(service.read({ resource: resource2 }).length, 1); + + // Verify resource1 markers are visible again + assert.strictEqual(service.read({ ignoreResourceFilters: true }).length, 2); + assert.strictEqual(service.read({ resource: resource1, ignoreResourceFilters: true }).length, 1); + assert.strictEqual(service.read({ resource: resource1, ignoreResourceFilters: true })[0].severity, MarkerSeverity.Error); + assert.strictEqual(service.read({ resource: resource2, ignoreResourceFilters: true }).length, 1); + assert.strictEqual(service.read({ resource: resource2, ignoreResourceFilters: true })[0].severity, MarkerSeverity.Error); + + // Dispose filter + filter.dispose(); + }); + test('resource filter affects all filter combinations', () => { service = new markerService.MarkerService(); const resource = URI.parse('file:///path/file.cs'); diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 223ffbdfc1f..e98300aa9fe 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -945,11 +945,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain throw new Error('Failed to get box model.'); } + const content = model.content; const margin = model.margin; - const x = margin[0]; - const y = margin[1] + 32.4 + 35; // 32.4 is height of the title bar, 35 is height of the tab bar - const width = margin[2] - margin[0]; - const height = margin[5] - margin[1]; + const x = Math.min(margin[0], content[0]); + const y = Math.min(margin[1], content[1]) + 32.4 + 35; // 32.4 is height of the title bar, 35 is height of the tab bar + const width = Math.max(margin[2] - margin[0], content[2] - content[0]); + const height = Math.max(margin[5] - margin[1], content[5] - content[1]); const matched = await debuggers.sendCommand('CSS.getMatchedStylesForNode', { nodeId }, sessionId); if (!matched) { diff --git a/src/vs/platform/prompts/common/config.ts b/src/vs/platform/prompts/common/config.ts index bfef2fdcf59..1b992112e08 100644 --- a/src/vs/platform/prompts/common/config.ts +++ b/src/vs/platform/prompts/common/config.ts @@ -132,7 +132,7 @@ export namespace PromptsConfig { * be clearly mapped to a boolean (e.g., `"true"`, `"TRUE"`, `"FaLSe"`, etc.), * `undefined` for rest of the values */ -function asBoolean(value: any): boolean | undefined { +export const asBoolean = (value: any): boolean | undefined => { if (typeof value === 'boolean') { return value; } @@ -151,4 +151,4 @@ function asBoolean(value: any): boolean | undefined { } return undefined; -} +}; diff --git a/src/vs/platform/prompts/test/common/config.test.ts b/src/vs/platform/prompts/test/common/config.test.ts index 5c6e6fd5b0a..a895d9b4849 100644 --- a/src/vs/platform/prompts/test/common/config.test.ts +++ b/src/vs/platform/prompts/test/common/config.test.ts @@ -34,6 +34,131 @@ const createMock = (value: T): IConfigurationService => { suite('PromptsConfig', () => { ensureNoDisposablesAreLeakedInTestSuite(); + suite('• enabled', () => { + test('• true', () => { + const configService = createMock(true); + + assert.strictEqual( + PromptsConfig.enabled(configService), + true, + 'Must read correct enablement value.', + ); + }); + + test('• false', () => { + const configService = createMock(false); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• null', () => { + const configService = createMock(null); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• string', () => { + const configService = createMock(''); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• true string', () => { + const configService = createMock('TRUE'); + + assert.strictEqual( + PromptsConfig.enabled(configService), + true, + 'Must read correct enablement value.', + ); + }); + + test('• false string', () => { + const configService = createMock('FaLsE'); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• number', () => { + const configService = createMock(randomInt(100)); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• NaN', () => { + const configService = createMock(NaN); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• bigint', () => { + const configService = createMock(BigInt(randomInt(100))); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• symbol', () => { + const configService = createMock(Symbol('test')); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• object', () => { + const configService = createMock({ + '.github/prompts': false, + }); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + + test('• array', () => { + const configService = createMock(['.github/prompts']); + + assert.strictEqual( + PromptsConfig.enabled(configService), + false, + 'Must read correct enablement value.', + ); + }); + }); + + suite('• getLocationsValue', () => { test('• undefined', () => { const configService = createMock(undefined); diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 60370bfdee3..fcba3594b60 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -1163,6 +1163,7 @@ export class InputBox extends QuickInput implements IInputBox { private _valueSelection: Readonly<[number, number]> | undefined; private valueSelectionUpdated = true; private _placeholder: string | undefined; + private _ariaLabel: string | undefined; private _password = false; private _prompt: string | undefined; private readonly onDidValueChangeEmitter = this._register(new Emitter()); @@ -1202,6 +1203,15 @@ export class InputBox extends QuickInput implements IInputBox { this.update(); } + get ariaLabel() { + return this._ariaLabel; + } + + set ariaLabel(ariaLabel: string | undefined) { + this._ariaLabel = ariaLabel; + this.update(); + } + get password() { return this._password; } @@ -1272,6 +1282,20 @@ export class InputBox extends QuickInput implements IInputBox { if (this.ui.inputBox.password !== this.password) { this.ui.inputBox.password = this.password; } + let ariaLabel = this.ariaLabel; + // Only set aria label to the input box placeholder if we actually have an input box. + if (!ariaLabel && visibilities.inputBox) { + ariaLabel = this.placeholder + ? this.title + ? `${this.placeholder} - ${this.title}` + : this.placeholder + : this.title + ? this.title + : 'input'; + } + if (this.ui.inputBox.ariaLabel !== ariaLabel) { + this.ui.inputBox.ariaLabel = ariaLabel || 'input'; + } } } diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index 3bdc360144a..02415e9a3c3 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -540,24 +540,7 @@ class UnixPtyHeuristics extends Disposable { return; } - // Calculate the command - currentCommand.command = this._hooks.isCommandStorageDisabled ? '' : this._terminal.buffer.active.getLine(currentCommand.commandStartMarker.line)?.translateToString(true, currentCommand.commandStartX, currentCommand.commandRightPromptStartX).trim(); - let y = currentCommand.commandStartMarker.line + 1; - const commandExecutedLine = currentCommand.commandExecutedMarker.line; - for (; y < commandExecutedLine; y++) { - const line = this._terminal.buffer.active.getLine(y); - if (line) { - const continuation = currentCommand.continuations?.find(e => e.marker.line === y); - if (continuation) { - currentCommand.command += '\n'; - } - const startColumn = continuation?.end ?? 0; - currentCommand.command += line.translateToString(true, startColumn); - } - } - if (y === commandExecutedLine) { - currentCommand.command += this._terminal.buffer.active.getLine(commandExecutedLine)?.translateToString(true, undefined, currentCommand.commandExecutedX) || ''; - } + currentCommand.command = this._capability.promptInputModel.ghostTextIndex > -1 ? this._capability.promptInputModel.value.substring(0, this._capability.promptInputModel.ghostTextIndex) : this._capability.promptInputModel.value; this._hooks.onCommandExecutedEmitter.fire(currentCommand as ITerminalCommand); } } diff --git a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts index df4491d4e86..a484e898e01 100644 --- a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts +++ b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts @@ -37,7 +37,7 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { private _forwardMarkers(resources: readonly URI[]): void { const data: [UriComponents, IMarkerData[]][] = []; for (const resource of resources) { - const allMarkerData = this._markerService.read({ resource }); + const allMarkerData = this._markerService.read({ resource, ignoreResourceFilters: true }); if (allMarkerData.length === 0) { data.push([resource, []]); } else { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index e82e936637e..735e7212679 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -619,10 +619,10 @@ const registry = Registry.as(ConfigurationExtensions.Con 'default': 'ui', 'scope': ConfigurationScope.WINDOW }, - 'workbench.settings.showExperimentalSuggestions': { + 'workbench.settings.showSuggestions': { 'type': 'boolean', 'default': false, - 'description': localize('settings.showExperimentalSuggestions', "Controls whether experimental suggestions are shown in the Settings editor. This setting requires a reload to take effect."), + 'description': localize('settings.showSuggestions', "Controls whether setting suggestions are shown below the search bar in the Settings editor."), 'tags': ['experimental'] }, 'workbench.hover.delay': { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 2f216cfdc31..5b879b4d9da 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -674,21 +674,21 @@ export function registerChatActions() { const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0; const completionsQuotaExceeded = chatEntitlementService.quotas.completions?.percentRemaining === 0; if (chatQuotaExceeded && !completionsQuotaExceeded) { - message = localize('chatQuotaExceeded', "You've reached your monthly chat requests quota. You still have free code completions available."); + message = localize('chatQuotaExceeded', "You've reached your monthly chat messages quota. You still have free code completions available."); } else if (completionsQuotaExceeded && !chatQuotaExceeded) { - message = localize('completionsQuotaExceeded', "You've reached your monthly code completions quota. You still have free chat requests available."); + message = localize('completionsQuotaExceeded', "You've reached your monthly code completions quota. You still have free chat messages available."); } else { - message = localize('chatAndCompletionsQuotaExceeded', "You've reached your monthly chat requests and code completions quota."); + message = localize('chatAndCompletionsQuotaExceeded', "You've reached your monthly chat messages and code completions quota."); } if (chatEntitlementService.quotas.resetDate) { const dateFormatter = safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' }); const quotaResetDate = new Date(chatEntitlementService.quotas.resetDate); - message = [message, localize('quotaResetDate', "The allowance will renew on {0}.", dateFormatter.format(quotaResetDate))].join(' '); + message = [message, localize('quotaResetDate', "The allowance will reset on {0}.", dateFormatter.format(quotaResetDate))].join(' '); } const limited = chatEntitlementService.entitlement === ChatEntitlement.Limited; - const upgradeToPro = limited ? localize('upgradeToPro', "Upgrade to Copilot Pro (your first 30 days are free) for:\n- Unlimited code completions\n- Unlimited basic chat requests\n- Access to premium models") : undefined; + const upgradeToPro = limited ? localize('upgradeToPro', "Upgrade to Copilot Pro (your first 30 days are free) for:\n- Unlimited code completions\n- Unlimited chat messages\n- Access to premium models") : undefined; await dialogService.prompt({ type: 'none', @@ -843,7 +843,7 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben primaryActionIcon = Codicon.copilotNotConnected; } else if (chatQuotaExceeded && limited) { primaryActionId = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG; - primaryActionTitle = localize('chatQuotaExceededButton', "Copilot Free plan chat requests quota reached. Click for details."); + primaryActionTitle = localize('chatQuotaExceededButton', "Copilot Free plan chat messages quota reached. Click for details."); primaryActionIcon = Codicon.copilotWarning; } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index b101670c286..dfb0dbb7638 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -38,7 +38,6 @@ import { ActiveEditorContext, TextCompareEditorActiveContext } from '../../../.. import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js'; import { IHostService } from '../../../../services/host/browser/host.js'; import { VIEW_ID as SEARCH_VIEW_ID } from '../../../../services/search/common/search.js'; import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js'; @@ -674,7 +673,6 @@ export class AttachContextAction extends Action2 { const clipboardService = accessor.get(IClipboardService); const editorService = accessor.get(IEditorService); const contextKeyService = accessor.get(IContextKeyService); - const extensionService = accessor.get(IExtensionService); const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); const chatEditingService = accessor.get(IChatEditingService); @@ -686,7 +684,7 @@ export class AttachContextAction extends Action2 { } const quickPickItems: IAttachmentQuickPickItem[] = []; - if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { + if (widget.input.selectedLanguageModel?.metadata.capabilities?.vision) { const imageData = await clipboardService.readImage(); if (isImage(imageData)) { quickPickItems.push({ @@ -984,7 +982,7 @@ async function createMarkersQuickPick(accessor: ServicesAccessor, onBackgroundAc const store = new DisposableStore(); const quickPick = store.add(quickInputService.createQuickPick({ useSeparators: true })); quickPick.canAcceptInBackground = !onBackgroundAccept; - quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...'); + quickPick.placeholder = localize('pickAProblem', 'Select a problem to attach'); quickPick.items = items; return new Promise(resolve => { diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts index 9e72b65eea7..bdd5b506f4f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/dialogs/askToSelectPrompt/promptFilePickers.ts @@ -78,6 +78,7 @@ const NEW_PROMPT_FILE_OPTION: WithUriValue = Object.freeze({ )}`, value: URI.parse(PROMPT_DOCUMENTATION_URL), pickable: false, + alwaysShow: true, buttons: [HELP_BUTTON], }); @@ -88,10 +89,11 @@ const NEW_INSTRUCTIONS_FILE_OPTION: WithUriValue = Object.freeze type: 'item', label: `$(plus) ${localize( 'commands.new-instructionsfile.select-dialog.label', - 'New instructions file...', + 'Create new instruction file...', )}`, value: URI.parse(INSTRUCTIONS_DOCUMENTATION_URL), pickable: false, + alwaysShow: true, buttons: [HELP_BUTTON], }); @@ -427,4 +429,3 @@ export class PromptFilePickers { } } - diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index c068dd4cf75..552061000be 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -47,7 +47,7 @@ import { ILanguageModelsService, LanguageModelsService } from '../common/languag import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL } from '../common/promptSyntax/constants.js'; -import { registerReusablePromptLanguageFeatures } from '../common/promptSyntax/languageFeatures/providers/index.js'; +import { registerPromptFileContributions } from '../common/promptSyntax/contributions/index.js'; import { PromptsService } from '../common/promptSyntax/service/promptsService.js'; import { IPromptsService } from '../common/promptSyntax/service/types.js'; import { LanguageModelToolsExtensionPointHandler } from '../common/tools/languageModelToolsContribution.js'; @@ -218,9 +218,15 @@ configurationRegistry.registerConfiguration({ type: 'boolean', tags: ['preview'] }, + 'chat.sendElementsToChat.attachCSS': { + default: true, + markdownDescription: nls.localize('chat.sendElementsToChat.attachCSS', "Controls whether CSS of the selected element will be added to the chat. {0} must be enabled.", '`#chat.sendElementsToChat.enabled#`'), + type: 'boolean', + tags: ['preview'] + }, 'chat.sendElementsToChat.attachImages': { default: true, - description: nls.localize('chat.sendElementsToChat.attachImages', "Controls whether a screenshot of the attached element will be added to the chat."), + markdownDescription: nls.localize('chat.sendElementsToChat.attachImages', "Controls whether a screenshot of the selected element will be added to the chat. {0} must be enabled.", '`#chat.sendElementsToChat.enabled#`'), type: 'boolean', tags: ['preview'] }, @@ -655,4 +661,4 @@ registerSingleton(IPromptsService, PromptsService, InstantiationType.Delayed); registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); -registerReusablePromptLanguageFeatures(); +registerPromptFileContributions(); diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts index 957cba60d4e..8c1bd1164f6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityProvider.ts @@ -11,11 +11,29 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js'; import { ChatTreeItem } from './chat.js'; import { isRequestVM, isResponseVM, IChatResponseViewModel } from '../common/chatViewModel.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { AcceptToolConfirmationActionId } from './actions/chatToolActions.js'; +import { CancelChatActionId } from './actions/chatExecuteActions.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; + +export const getToolConfirmationAlert = (accessor: ServicesAccessor, title: string) => { + const keybindingService = accessor.get(IKeybindingService); + const contextKeyService = accessor.get(IContextKeyService); + + const acceptKb = keybindingService.lookupKeybinding(AcceptToolConfirmationActionId, contextKeyService)?.getAriaLabel(); + const cancelKb = keybindingService.lookupKeybinding(CancelChatActionId, contextKeyService)?.getAriaLabel(); + + return acceptKb && cancelKb + ? localize('toolInvocationsHintKb', "Action required to confirm tool action: {0}. Press {1} to accept or {2} to cancel.", title, acceptKb, cancelKb) + : localize('toolInvocationsHint', "Action required to confirm tool action: {0}", title); +}; export class ChatAccessibilityProvider implements IListAccessibilityProvider { constructor( - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { } getWidgetRole(): AriaRole { @@ -46,12 +64,19 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider v.kind === 'toolInvocation').filter(v => !v.isComplete); + const toolInvocation = element.response.value.filter(v => v.kind === 'toolInvocation'); let toolInvocationHint = ''; if (toolInvocation.length) { - const titles = toolInvocation.map(v => v.confirmationMessages?.title).filter(v => !!v); - if (titles.length) { - toolInvocationHint = localize('toolInvocationsHint', "Action required: {0} ", titles.join(', ')); + const waitingForConfirmation = toolInvocation.filter(v => !v.isComplete); + if (waitingForConfirmation.length) { + const titles = toolInvocation.map(v => v.confirmationMessages?.title).filter(v => !!v); + if (titles.length) { + toolInvocationHint = this._instantiationService.invokeFunction(getToolConfirmationAlert, titles.join(', ')); + } + } else { // all completed + for (const invocation of toolInvocation) { + toolInvocationHint += localize('toolCompletedHint', "Tool {0} completed.", invocation.confirmationMessages?.title); + } } } const tableCount = marked.lexer(element.response.toString()).filter(token => token.type === 'table')?.length ?? 0; diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index 7801b63b223..11d44a0edb1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -26,31 +26,29 @@ export interface IChatAttachmentChangeEvent { } export class ChatAttachmentModel extends Disposable { - /** - * Collection on prompt instruction attachments. - */ - public readonly promptInstructions: ChatPromptAttachmentsCollection; + + readonly promptInstructions: ChatPromptAttachmentsCollection; + private readonly _attachments = new Map(); + + private _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; constructor( - @IInstantiationService private readonly initService: IInstantiationService, + @IInstantiationService instaService: IInstantiationService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService, @ISharedWebContentExtractorService private readonly webContentExtractorService: ISharedWebContentExtractorService, ) { super(); - this.promptInstructions = this._register( - this.initService.createInstance(ChatPromptAttachmentsCollection), - ); + this.promptInstructions = this._register(instaService.createInstance(ChatPromptAttachmentsCollection)); } - private _attachments = new Map(); get attachments(): ReadonlyArray { return Array.from(this._attachments.values()); } - private _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; + get size(): number { return this._attachments.size; @@ -65,9 +63,7 @@ export class ChatAttachmentModel extends Disposable { return new Set(this._attachments.keys()); } - clear( - clearStickyAttachments: boolean = false, - ): void { + clear(clearStickyAttachments: boolean = false): void { const deleted = Array.from(this._attachments.keys()); this._attachments.clear(); diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts index fefca2429fd..16b2d8d00cf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentResolve.ts @@ -193,6 +193,10 @@ function getMimeTypeFromPath(match: RegExpExecArray): string | undefined { return MIME_TYPES[ext]; } +export function getAttachableImageExtension(mimeType: string): string | undefined { + return Object.entries(MIME_TYPES).find(([_, value]) => value === mimeType)?.[0]; +} + // --- MARKERS --- export function resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[] { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts index a3929a2dfd9..b354718cfbb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts @@ -11,6 +11,7 @@ import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { autorun, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { localize } from '../../../../../nls.js'; @@ -18,23 +19,32 @@ import { IContextKeyService } from '../../../../../platform/contextkey/common/co import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IChatRendererContent } from '../../common/chatViewModel.js'; import { ChatTreeItem, IChatCodeBlockInfo } from '../chat.js'; +import { getAttachableImageExtension } from '../chatAttachmentResolve.js'; import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions } from '../codeBlockPart.js'; +import { ChatAttachmentsContentPart } from './chatAttachmentsContentPart.js'; import { IDisposableReference } from './chatCollections.js'; import { ChatQueryTitlePart } from './chatConfirmationWidget.js'; import { IChatContentPartRenderContext } from './chatContentParts.js'; import { EditorPool } from './chatMarkdownContentPart.js'; export interface IChatCollapsibleIOCodePart { + kind: 'code'; textModel: ITextModel; languageId: string; options: ICodeBlockRenderOptions; codeBlockInfo: IChatCodeBlockInfo; } +export interface IChatCollapsibleIODataPart { + kind: 'data'; + value: Uint8Array; + mimeType: string; +} + export interface IChatCollapsibleInputData extends IChatCollapsibleIOCodePart { } export interface IChatCollapsibleOutputData { // todo: show images etc. here - parts: IChatCollapsibleIOCodePart[]; + parts: (IChatCollapsibleIOCodePart | IChatCollapsibleIODataPart)[]; } export class ChatCollapsibleInputOutputContentPart extends Disposable { @@ -72,24 +82,26 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { isError: boolean, initiallyExpanded: boolean, @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); const elements = dom.h('.chat-confirmation-widget@root', [ - dom.h('.chat-confirmation-widget-title.expandable@title', [ + dom.h('.chat-confirmation-widget-title.expandable@titleContainer', [ dom.h('.chat-confirmation-widget-expando@expando'), + dom.h('.chat-confirmation-widget-title-inner@title'), + dom.h('.chat-confirmation-widget-title-icon@icon'), ]), dom.h('.chat-confirmation-widget-message@message'), ]); this.domNode = elements.root; - const titlePart = this._titlePart = this._register(instantiationService.createInstance( + const titlePart = this._titlePart = this._register(_instantiationService.createInstance( ChatQueryTitlePart, elements.title, title, subtitle, - instantiationService.createInstance(MarkdownRenderer, {}), + _instantiationService.createInstance(MarkdownRenderer, {}), )); this._register(titlePart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); @@ -102,7 +114,7 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { ? ThemeIcon.asCSSSelector(Codicon.check) : ThemeIcon.asCSSSelector(ThemeIcon.modify(Codicon.loading, 'spin')) ); - elements.title.appendChild(check.root); + elements.icon.appendChild(check.root); const expanded = this._expanded = observableValue(this, initiallyExpanded); const btn = this._register(new Button(elements.expando, {})); @@ -114,10 +126,16 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { this._onDidChangeHeight.fire(); })); - this._register(dom.addDisposableGenericMouseDownListener(elements.title, () => { - const value = expanded.get(); - expanded.set(!value, undefined); - })); + const toggle = (e: Event) => { + if (!e.defaultPrevented) { + const value = expanded.get(); + expanded.set(!value, undefined); + e.preventDefault(); + } + }; + + this._register(btn.onDidClick(toggle)); + this._register(dom.addDisposableListener(elements.titleContainer, dom.EventType.CLICK, toggle)); elements.message.appendChild(this.createMessageContents()); } @@ -141,7 +159,17 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { } else { contents.outputTitle.textContent = localize('chat.output', "Output"); for (const part of output.parts) { - this.addCodeBlock(part, contents.output); + if (part.kind === 'data' && getAttachableImageExtension(part.mimeType)) { + const n = this._register(this._instantiationService.createInstance( + ChatAttachmentsContentPart, + [{ kind: 'image', id: generateUuid(), name: `image.${getAttachableImageExtension(part.mimeType)}`, value: part.value, mimeType: part.mimeType, isURL: false }], + undefined, + undefined, + )); + contents.output.appendChild(n.domNode!); + } else if (part.kind === 'code') { + this.addCodeBlock(part, contents.output); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts index fa13aeeed7d..55d880b09df 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInvocationPart.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; +import { assertNever } from '../../../../../base/common/assert.js'; import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { decodeBase64 } from '../../../../../base/common/buffer.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter } from '../../../../../base/common/event.js'; import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; @@ -12,6 +14,7 @@ import { Disposable, DisposableStore, IDisposable, thenIfNotDisposed, toDisposab import { autorunWithStore } from '../../../../../base/common/observable.js'; import { count } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { isEmptyObject } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; @@ -28,17 +31,18 @@ import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatMarkdownContent, IChatProgressMessage, IChatTerminalToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js'; import { IChatRendererContent } from '../../common/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js'; -import { createToolInputUri, createToolSchemaUri, ILanguageModelToolsService, isToolResultInputOutputDetails } from '../../common/languageModelToolsService.js'; +import { createToolInputUri, createToolSchemaUri, ILanguageModelToolsService, isToolResultInputOutputDetails, IToolResultInputOutputDetails } from '../../common/languageModelToolsService.js'; import { CancelChatActionId } from '../actions/chatExecuteActions.js'; import { AcceptToolConfirmationActionId } from '../actions/chatToolActions.js'; import { ChatTreeItem, IChatCodeBlockInfo } from '../chat.js'; +import { getAttachableImageExtension } from '../chatAttachmentResolve.js'; import { ICodeBlockRenderOptions } from '../codeBlockPart.js'; import { ChatConfirmationWidget, ChatCustomConfirmationWidget, IChatConfirmationButton } from './chatConfirmationWidget.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; import { ChatMarkdownContentPart, EditorPool } from './chatMarkdownContentPart.js'; import { ChatCustomProgressPart, ChatProgressContentPart } from './chatProgressContentPart.js'; import { ChatCollapsibleListContentPart, CollapsibleListPool, IChatCollapsibleListItem } from './chatReferencesContentPart.js'; -import { ChatCollapsibleInputOutputContentPart, IChatCollapsibleIOCodePart } from './chatToolInputOutputContentPart.js'; +import { ChatCollapsibleInputOutputContentPart, IChatCollapsibleIOCodePart, IChatCollapsibleIODataPart } from './chatToolInputOutputContentPart.js'; export class ChatToolInvocationPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; @@ -236,7 +240,7 @@ class ChatToolInvocationSubPart extends Disposable { dom.h('.editor@editor'), ]); - if (toolInvocation.toolSpecificData?.kind === 'input') { + if (toolInvocation.toolSpecificData?.kind === 'input' && toolInvocation.toolSpecificData.rawInput && !isEmptyObject(toolInvocation.toolSpecificData.rawInput)) { const inputData = toolInvocation.toolSpecificData; @@ -539,7 +543,7 @@ class ChatToolInvocationSubPart extends Disposable { return progressPart.domNode; } - private createInputOutputMarkdownProgressPart(message: string | IMarkdownString, subtitle: string | IMarkdownString | undefined, input: string, output: string | undefined, isError: boolean): HTMLElement { + private createInputOutputMarkdownProgressPart(message: string | IMarkdownString, subtitle: string | IMarkdownString | undefined, input: string, output: IToolResultInputOutputDetails['output'] | undefined, isError: boolean): HTMLElement { let codeBlockIndex = this.codeBlockStartIndex; const toCodePart = (data: string): IChatCollapsibleIOCodePart => { const model = this._register(this.modelService.createModel( @@ -548,6 +552,7 @@ class ChatToolInvocationSubPart extends Disposable { )); return { + kind: 'code', textModel: model, languageId: model.getLanguageId(), options: { @@ -573,6 +578,10 @@ class ChatToolInvocationSubPart extends Disposable { }; }; + if (typeof output === 'string') { // back compat with older stored versions + output = [{ type: 'text', value: output }]; + } + const collapsibleListPart = this._register(this.instantiationService.createInstance( ChatCollapsibleInputOutputContentPart, message, @@ -580,7 +589,22 @@ class ChatToolInvocationSubPart extends Disposable { this.context, this.editorPool, toCodePart(input), - output ? { parts: [toCodePart(output)] } : undefined, + output && { + parts: output.map((o): IChatCollapsibleIODataPart | IChatCollapsibleIOCodePart => { + if (o.type === 'data') { + const decoded = decodeBase64(o.value64).buffer; + if (getAttachableImageExtension(o.mimeType)) { + return { kind: 'data', value: decoded, mimeType: o.mimeType }; + } else { + return toCodePart(localize('toolResultData', "Data of type {0} ({1} bytes)", o.mimeType, decoded.byteLength)); + } + } else if (o.type === 'text') { + return toCodePart(o.value); + } else { + assertNever(o); + } + }), + }, isError, ChatToolInvocationSubPart._expandedByDefault.get(this.toolInvocation) ?? false, )); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css index 5672cea19b7..76f390053ac 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css @@ -36,6 +36,15 @@ background: var(--vscode-toolbar-hoverBackground); } +.chat-confirmation-widget .chat-confirmation-widget-title-inner { + flex-grow: 1; + flex-basis: 0; +} + +.chat-confirmation-widget .chat-confirmation-widget-title-icon { + line-height: 0; +} + .chat-confirmation-widget .chat-confirmation-widget-title p, .chat-confirmation-widget .chat-confirmation-widget-title .rendered-markdown { display: inline; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts index 18dcd87c941..8d892252d3a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/simpleBrowserEditorOverlay.ts @@ -32,6 +32,7 @@ import { IEnvironmentService } from '../../../../../platform/environment/common/ import { URI } from '../../../../../base/common/uri.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; +import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; class SimpleBrowserOverlayWidget { @@ -53,8 +54,19 @@ class SimpleBrowserOverlayWidget { @IEnvironmentService private readonly environmentService: IEnvironmentService, @ILogService private readonly logService: ILogService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IPreferencesService private readonly _preferencesService: IPreferencesService, ) { + this._showStore.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('chat.sendElementsToChat.enabled')) { + if (this.configurationService.getValue('chat.sendElementsToChat.enabled')) { + this.showElement(this._domNode); + } else { + this.hideElement(this._domNode); + } + } + })); + this.imagesFolder = joinPath(this.environmentService.workspaceStorageHome, 'vscode-chat-images'); cleanupOldImages(this.fileService, this.logService, this.imagesFolder); @@ -69,12 +81,15 @@ class SimpleBrowserOverlayWidget { let cts: CancellationTokenSource; const selectButton = this._showStore.add(new Button(this._domNode, { ...defaultButtonStyles, supportIcons: true, title: localize('selectAnElement', 'Click to select an element.') })); selectButton.element.className = 'element-selection-start'; - selectButton.label = localize('startSelection', 'Start Selection'); + selectButton.label = localize('startSelection', 'Start'); const cancelButton = this._showStore.add(new Button(this._domNode, { ...defaultButtonStyles, supportIcons: true, title: localize('cancelSelection', 'Click to cancel selection.') })); cancelButton.element.className = 'element-selection-cancel hidden'; cancelButton.label = localize('cancel', 'Cancel'); + const configure = this._showStore.add(new Button(this._domNode, { supportIcons: true, title: localize('chat.configureElements', "Configure Attachments Sent") })); + configure.icon = Codicon.gear; + const collapseOverlay = this._showStore.add(new Button(this._domNode, { supportIcons: true, title: localize('chat.hideOverlay', "Collapse Overlay") })); collapseOverlay.icon = Codicon.chevronRight; @@ -114,7 +129,7 @@ class SimpleBrowserOverlayWidget { this._editor.focus(); // start selection - message.textContent = localize('elementSelectionInProgress', 'Selection in progress...'); + message.textContent = localize('elementSelectionInProgress', 'Selecting element...'); this.hideElement(selectButton.element); this.showElement(cancelButton.element); await this.addElementToChat(cts); @@ -145,6 +160,10 @@ class SimpleBrowserOverlayWidget { message.textContent = startSelectionMessage; resetButtons(); })); + + this._showStore.add(addDisposableListener(configure.element, 'click', () => { + this._preferencesService.openSettings({ jsonEditor: false, query: '@id:chat.sendElementsToChat.enabled,chat.sendElementsToChat.attachCSS,chat.sendElementsToChat.attachImages' }); + })); } hideElement(element: HTMLElement) { @@ -168,26 +187,29 @@ class SimpleBrowserOverlayWidget { throw new Error('Element data not found'); } const bounds = elementData.bounds; - - // remove container so we don't block anything on screenshot - this._domNode.style.display = 'none'; - - // Wait 1 extra frame to make sure overlay is gone - await new Promise(resolve => setTimeout(resolve, 100)); - const toAttach: IChatRequestVariableEntry[] = []; const widget = this._chatWidgetService.lastFocusedWidget ?? await showChatView(this._viewService); + let value = 'Attached HTML and CSS Context\n\n' + elementData.outerHTML; + if (this.configurationService.getValue('chat.sendElementsToChat.attachCSS')) { + value += '\n\n' + elementData.computedStyle; + } toAttach.push({ id: 'element-' + Date.now(), name: this.getDisplayNameFromOuterHTML(elementData.outerHTML), fullName: this.getDisplayNameFromOuterHTML(elementData.outerHTML), - value: elementData.outerHTML + elementData.computedStyle, + value: value, kind: 'element', icon: ThemeIcon.fromId(Codicon.layout.id), }); if (this.configurationService.getValue('chat.sendElementsToChat.attachImages')) { + // remove container so we don't block anything on screenshot + this._domNode.style.display = 'none'; + + // Wait 1 extra frame to make sure overlay is gone + await new Promise(resolve => setTimeout(resolve, 100)); + const screenshot = await this._hostService.getScreenshot(bounds); if (!screenshot) { throw new Error('Screenshot failed'); @@ -201,10 +223,11 @@ class SimpleBrowserOverlayWidget { value: screenshot.buffer, references: fileReference ? [{ reference: fileReference, kind: 'reference' }] : [], }); + + this._domNode.style.display = ''; } widget?.attachmentModel?.addContext(...toAttach); - this._domNode.style.display = ''; } @@ -244,6 +267,10 @@ class SimpleBrowserOverlayController { @IConfigurationService private readonly configurationService: IConfigurationService, ) { + if (!this.configurationService.getValue('chat.sendElementsToChat.enabled')) { + return; + } + this._domNode.classList.add('chat-simple-browser-overlay'); this._domNode.style.position = 'absolute'; this._domNode.style.bottom = `5px`; @@ -275,9 +302,6 @@ class SimpleBrowserOverlayController { const editor = group.activeEditorPane; if (editor?.input.editorId === 'mainThreadWebview-simpleBrowser.view') { - if (!this.configurationService.getValue('chat.sendElementsToChat.enabled')) { - return undefined; - } const uri = EditorResourceAccessor.getOriginalUri(editor?.input, { supportSideBySide: SideBySideEditor.PRIMARY }); return uri; } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 515b257dfa6..63ffa4dd5ae 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -299,11 +299,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private modelWidget: ModelPickerActionItem | undefined; private readonly _waitForPersistedLanguageModel = this._register(new MutableDisposable()); private _onDidChangeCurrentLanguageModel = this._register(new Emitter()); + private _currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined; get currentLanguageModel() { return this._currentLanguageModel?.identifier; } + get selectedLanguageModel(): ILanguageModelChatMetadataAndIdentifier | undefined { + return this._currentLanguageModel; + } + private _onDidChangeCurrentChatMode = this._register(new Emitter()); readonly onDidChangeCurrentChatMode = this._onDidChangeCurrentChatMode.event; diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index 7ca30ffa337..72b81354148 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -5,6 +5,7 @@ import { renderMarkdownAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { stripIcons } from '../../../../base/common/iconLabels.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -68,7 +69,7 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi const toolInvocation = item.response.value.find(item => item.kind === 'toolInvocation'); if (toolInvocation?.confirmationMessages) { const title = toolInvocation.confirmationMessages.title; - const message = typeof toolInvocation.confirmationMessages.message === 'string' ? toolInvocation.confirmationMessages.message : toolInvocation.confirmationMessages.message.value; + const message = typeof toolInvocation.confirmationMessages.message === 'string' ? toolInvocation.confirmationMessages.message : stripIcons(renderMarkdownAsPlaintext(toolInvocation.confirmationMessages.message)); const terminalCommand = toolInvocation.toolSpecificData && 'command' in toolInvocation.toolSpecificData ? toolInvocation.toolSpecificData.command : undefined; responseContent += `${title}`; if (terminalCommand) { diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index 77739b4f599..50c44bb8d23 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -311,19 +311,18 @@ class ChatStatusDashboard extends Disposable { run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageSettingsUrl))), })); - const completionsQuotaIndicator = completionsQuota ? this.createQuotaIndicator(this.element, completionsQuota, localize('completionsLabel', "Code completions"), false) : undefined; - const chatQuotaIndicator = chatQuota ? this.createQuotaIndicator(this.element, chatQuota, premiumChatQuota ? localize('basicChatsLabel', "Basic chat requests") : localize('chatsLabel', "Chat requests"), false) : undefined; - const premiumChatQuotaIndicator = premiumChatQuota ? this.createQuotaIndicator(this.element, premiumChatQuota, localize('premiumChatsLabel', "Premium chat requests"), true) : undefined; + const completionsQuotaIndicator = completionsQuota ? this.createQuotaIndicator(this.element, disposables, completionsQuota, localize('completionsLabel', "Code completions"), false) : undefined; + const chatQuotaIndicator = chatQuota ? this.createQuotaIndicator(this.element, disposables, chatQuota, localize('chatsLabel', "Chat messages"), false) : undefined; + const premiumChatQuotaIndicator = premiumChatQuota ? this.createQuotaIndicator(this.element, disposables, premiumChatQuota, localize('premiumChatsLabel', "Premium requests"), true) : undefined; if (resetDate) { - this.element.appendChild($('div.description', undefined, localize('limitQuota', "Allowance renews on {0}.", this.dateFormatter.value.format(new Date(resetDate))))); + this.element.appendChild($('div.description', undefined, localize('limitQuota', "Allowance resets {0}.", this.dateFormatter.value.format(new Date(resetDate))))); } - const limited = this.chatEntitlementService.entitlement === ChatEntitlement.Limited; - if ((limited && (chatQuota?.percentRemaining === 0 || completionsQuota?.percentRemaining === 0)) || (!limited && typeof premiumChatQuota?.percentRemaining === 'number' && premiumChatQuota.percentRemaining <= 25 && !premiumChatQuota.overageEnabled)) { - const button = disposables.add(new Button(this.element, { ...defaultButtonStyles, secondary: canUseCopilot(this.chatEntitlementService) /* use secondary color when copilot can still be used */ })); - button.label = limited ? localize('upgradeToCopilotPro', "Upgrade to Copilot Pro") : localize('enableAdditionalUsage', "Enable Additional Premium Requests"); - disposables.add(button.onDidClick(() => this.runCommandAndClose(limited ? 'workbench.action.chat.upgradePlan' : () => this.openerService.open(URI.parse(defaultChat.manageOverageUrl))))); + if (this.chatEntitlementService.entitlement === ChatEntitlement.Limited && (Number(chatQuota?.percentRemaining) <= 25 || Number(completionsQuota?.percentRemaining) <= 25)) { + const upgradeProButton = disposables.add(new Button(this.element, { ...defaultButtonStyles, secondary: canUseCopilot(this.chatEntitlementService) /* use secondary color when copilot can still be used */ })); + upgradeProButton.label = localize('upgradeToCopilotPro', "Upgrade to Copilot Pro"); + disposables.add(upgradeProButton.onDidClick(() => this.runCommandAndClose('workbench.action.chat.upgradePlan'))); } (async () => { @@ -420,7 +419,7 @@ class ChatStatusDashboard extends Disposable { id: 'workbench.action.openChatStatusItemLink', label: localize('learnMore', "Learn More"), tooltip: localize('learnMore', "Learn More"), - class: ThemeIcon.asClassName(Codicon.question), + class: ThemeIcon.asClassName(Codicon.linkExternal), run: () => this.runCommandAndClose(() => this.openerService.open(URI.parse(headerLink))), }) : undefined); @@ -459,7 +458,7 @@ class ChatStatusDashboard extends Disposable { this.hoverService.hideHover(true); } - private createQuotaIndicator(container: HTMLElement, quota: IQuotaSnapshot, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot) => void { + private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot) => void { const quotaValue = $('span.quota-value'); const quotaBit = $('div.quota-bit'); const overageLabel = $('span.overage-label'); @@ -472,40 +471,49 @@ class ChatStatusDashboard extends Disposable { $('div.quota-bar', undefined, quotaBit ), - $('div.overage', undefined, + $('div.description', undefined, overageLabel ) )); + if (supportsOverage) { + const manageOverageButton = disposables.add(new Button(quotaIndicator, { ...defaultButtonStyles, secondary: true })); + manageOverageButton.label = localize('enableAdditionalUsage', "Manage paid premium requests"); + disposables.add(manageOverageButton.onDidClick(() => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageOverageUrl))))); + } + const update = (quota: IQuotaSnapshot) => { quotaIndicator.classList.remove('error'); quotaIndicator.classList.remove('warning'); - quotaIndicator.classList.remove('unlimited'); + + let usedPercentage: number; + if (quota.unlimited) { + usedPercentage = 0; + } else { + usedPercentage = Math.max(0, 100 - quota.percentRemaining); + } if (quota.unlimited) { - quotaIndicator.classList.add('unlimited'); quotaValue.textContent = localize('quotaUnlimited', "Included"); + } else if (quota.overageCount) { + quotaValue.textContent = localize('quotaDisplayWithOverage', "+{0} requests", quota.overageCount); } else { - let usedPercentage = Math.max(0, 100 - quota.percentRemaining); - if (usedPercentage === 0) { - usedPercentage = 1; // indicate minimal usage as 1% - } + quotaValue.textContent = localize('quotaDisplay', "{0}%", usedPercentage); + } - quotaValue.textContent = quota.overageCount ? localize('quotaDisplayWithOverage', "+{0} paid", quota.overageCount) : localize('quotaDisplay', "{0}%", usedPercentage); - quotaBit.style.width = `${usedPercentage}%`; + quotaBit.style.width = `${usedPercentage}%`; - if (usedPercentage >= 90 && !quota.overageEnabled) { - quotaIndicator.classList.add('error'); - } else if (usedPercentage >= 75) { - quotaIndicator.classList.add('warning'); - } + if (usedPercentage >= 90) { + quotaIndicator.classList.add('error'); + } else if (usedPercentage >= 75) { + quotaIndicator.classList.add('warning'); } if (supportsOverage) { if (quota.overageEnabled) { - overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests are enabled."); + overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests enabled."); } else { - overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests are disabled."); + overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled."); } } else { overageLabel.textContent = ''; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 3efc62a14c8..dcb3c7e4c6c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -14,6 +14,7 @@ import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Iterable } from '../../../../base/common/iterator.js'; import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; @@ -1035,7 +1036,12 @@ export class ChatWidget extends Disposable implements IChatWidget { this.refreshParsedInput(); })); this._register(autorun(r => { - this.input.selectedToolsModel.tools.read(r); // SIGNAL + const enabledTools = new Set(this.input.selectedToolsModel.tools.read(r).map(t => t.id)); + const disabledTools = this.inputPart.attachmentModel.attachments + .filter(a => a.kind === 'tool' && !enabledTools.has(a.id)) + .map(a => a.id); + + this.inputPart.attachmentModel.updateContent(disabledTools, Iterable.empty()); this.refreshParsedInput(); })); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 1e9f6118492..9423d3b995e 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -260,7 +260,7 @@ export async function createFilesAndFolderQuickPick(accessor: ServicesAccessor): defaultItems.sort((a, b) => extUri.compare(a.resource, b.resource)); const quickPick = quickInputService.createQuickPick(); - quickPick.placeholder = 'Search folder by name'; + quickPick.placeholder = 'Search file or folder by name'; quickPick.items = defaultItems; return await new Promise(_resolve => { diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 8e61ee3d41e..c9529b37743 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { renderStringAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; +import { assertNever } from '../../../../base/common/assert.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; +import { encodeBase64 } from '../../../../base/common/buffer.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; @@ -13,7 +15,6 @@ import { Iterable } from '../../../../base/common/iterator.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; -import { localize } from '../../../../nls.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -30,7 +31,8 @@ import { ChatModel } from '../common/chatModel.js'; import { ChatToolInvocation } from '../common/chatProgressTypes/chatToolInvocation.js'; import { IChatService } from '../common/chatService.js'; import { ChatConfiguration } from '../common/constants.js'; -import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, stringifyPromptTsxPart } from '../common/languageModelToolsService.js'; +import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, stringifyPromptTsxPart } from '../common/languageModelToolsService.js'; +import { getToolConfirmationAlert } from './chatAccessibilityProvider.js'; const jsonSchemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -264,7 +266,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo model.acceptResponseProgress(request, toolInvocation); if (prepared?.confirmationMessages) { - this._accessibilityService.alert(localize('toolConfirmationMessage', "Action required: {0}", prepared.confirmationMessages.title)); + this._accessibilityService.alert(this._instantiationService.invokeFunction(getToolConfirmationAlert, prepared.confirmationMessages.title)); const userConfirmed = await toolInvocation.confirmed.p; if (!userConfirmed) { throw new CancellationError(); @@ -324,7 +326,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo toolResult ??= { content: [] }; toolResult.toolResultError = err instanceof Error ? err.message : String(err); if (tool.data.alwaysDisplayInputOutput) { - toolResult.toolResultDetails = { input: this.formatToolInput(dto), output: String(err), isError: true }; + toolResult.toolResultDetails = { input: this.formatToolInput(dto), output: [{ type: 'text', value: String(err) }], isError: true }; } throw err; @@ -362,7 +364,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (!toolResult.toolResultDetails && toolData.alwaysDisplayInputOutput) { toolResult.toolResultDetails = { input: this.formatToolInput(dto), - output: this.toolResultToString(toolResult), + output: this.toolResultToIO(toolResult), }; } } @@ -371,18 +373,18 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return JSON.stringify(dto.parameters, undefined, 2); } - private toolResultToString(toolResult: IToolResult): string { - const strs = []; - for (const part of toolResult.content) { + private toolResultToIO(toolResult: IToolResult): IToolResultInputOutputDetails['output'] { + return toolResult.content.map(part => { if (part.kind === 'text') { - strs.push(part.value); + return { type: 'text', value: part.value }; } else if (part.kind === 'promptTsx') { - strs.push(stringifyPromptTsxPart(part)); + return { type: 'text', value: stringifyPromptTsxPart(part) }; } else if (part.kind === 'data') { - strs.push(`\n\n${localize('toolResultData', "Tool result data of type {0} ({1} bytes)", part.value.mimeType, part.value.data.byteLength)}\n\n`); + return { type: 'data', value64: encodeBase64(part.value.data), mimeType: part.value.mimeType }; + } else { + assertNever(part); } - } - return strs.join(''); + }); } private shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 238c1a4b023..31f8709ebff 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -1674,7 +1674,8 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 0 3px; } -.interactive-item-container .chat-confirmation-widget .interactive-result-code-block { +.interactive-item-container .chat-confirmation-widget .interactive-result-code-block, +.interactive-item-container .chat-confirmation-widget .chat-attached-context { margin-bottom: 8px; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index 55f193f87ef..ee1f9c7c300 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -28,6 +28,7 @@ } .chat-status-bar-entry-tooltip div.description { + font-size: 11px; color: var(--vscode-descriptionForeground); } @@ -71,10 +72,6 @@ margin: 4px 0; } -.chat-status-bar-entry-tooltip .quota-indicator.unlimited .quota-bar { - display: none; -} - .chat-status-bar-entry-tooltip .quota-indicator .quota-bar .quota-bit { height: 100%; background-color: var(--vscode-gauge-background); diff --git a/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css b/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css index bd5ce1394a9..ad5d4e5f4d2 100644 --- a/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/simpleBrowserOverlay.css @@ -8,7 +8,7 @@ position: absolute; bottom: 10px; right: 10px; - padding: 8px 10px; + padding: 0px 10px; background: var(--vscode-notifications-background); color: var(--vscode-notifications-foreground); border-radius: 4px; @@ -18,7 +18,8 @@ align-items: center; gap: 8px; width: max-content; - z-index: 1 + z-index: 1; + height: 42px; } .element-selection-message { @@ -27,19 +28,20 @@ } .element-expand-container { - bottom: 18px; + bottom: 15px; right: 15px; } .element-selection-cancel, .element-selection-start { - padding: 3px 8px; + padding: 2px 8px; width: fit-content; } .element-selection-message .monaco-button.codicon.codicon-close, .element-expand-container .monaco-button.codicon.codicon-layout, -.element-selection-message .monaco-button.codicon.codicon-chevron-right { +.element-selection-message .monaco-button.codicon.codicon-chevron-right, +.element-selection-message .monaco-button.codicon.codicon-gear { width: 17px; height: 17px; padding: 2px 2px; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts index 2a3e4c6cff9..9908f01032f 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/createPromptCommand.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isEqual } from '../../../../../../../base/common/resources.js'; +import { URI } from '../../../../../../../base/common/uri.js'; import { getCodeEditor } from '../../../../../../../editor/browser/editorBrowser.js'; import { SnippetController2 } from '../../../../../../../editor/contrib/snippet/browser/snippetController2.js'; import { localize } from '../../../../../../../nls.js'; @@ -39,6 +40,7 @@ const command = async ( accessor: ServicesAccessor, type: TPromptsType, ): Promise => { + const logService = accessor.get(ILogService); const fileService = accessor.get(IFileService); const labelService = accessor.get(ILabelService); @@ -77,7 +79,7 @@ const command = async ( return; } - const fileName = await askForPromptFileName(type, quickInputService); + const fileName = await askForPromptFileName(type, selectedFolder.uri, quickInputService, fileService); if (!fileName) { return; } @@ -130,7 +132,7 @@ const command = async ( Severity.Info, localize( 'workbench.command.prompts.create.user.enable-sync-notification', - "User prompts and instructions are not currently synchronized. Do you want to enable synchronization of the user prompts and instructions?", + "Do you want to backup and sync your user prompt and instruction files with Setting Sync?'", ), [ { @@ -141,7 +143,13 @@ const command = async ( logService.error(`Failed to run '${CONFIGURE_SYNC_COMMAND_ID}' command: ${error}.`); }); }, - } + }, + { + label: localize('learnMore.capitalized', "Learn More"), + run: () => { + openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); + }, + }, ], { neverShowAgain: { diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts index 494dcf0ce7e..2ac54236cf9 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/createPromptCommand/dialogs/askForPromptName.ts @@ -7,32 +7,68 @@ import { localize } from '../../../../../../../../nls.js'; import { TPromptsType } from '../../../../../common/promptSyntax/service/types.js'; import { getPromptFileExtension } from '../../../../../../../../platform/prompts/common/constants.js'; import { IQuickInputService } from '../../../../../../../../platform/quickinput/common/quickInput.js'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { IFileService } from '../../../../../../../../platform/files/common/files.js'; +import Severity from '../../../../../../../../base/common/severity.js'; +import { isValidBasename } from '../../../../../../../../base/common/extpath.js'; /** * Asks the user for a file name. */ export const askForPromptFileName = async ( type: TPromptsType, + selectedFolder: URI, quickInputService: IQuickInputService, + fileService: IFileService, ): Promise => { const placeHolder = (type === 'instructions') ? localize('askForInstructionsFileName.placeholder', "Enter the name of the instructions file") : localize('askForPromptFileName.placeholder', "Enter the name of the prompt file"); - const result = await quickInputService.input({ placeHolder }); + + const sanitizeInput = (input: string) => { + const trimmedName = input.trim(); + if (!trimmedName) { + return undefined; + } + + const fileExtension = getPromptFileExtension(type); + return (trimmedName.endsWith(fileExtension)) + ? trimmedName + : `${trimmedName}${fileExtension}`; + }; + + const validateInput = async (value: string) => { + const fileName = sanitizeInput(value); + if (!fileName) { + return { + content: localize('askForPromptFileName.error.empty', "Please enter a name."), + severity: Severity.Warning + }; + } + + if (!isValidBasename(fileName)) { + return { + content: localize('askForPromptFileName.error.invalid', "The name contains invalid characters."), + severity: Severity.Error + }; + } + + const fileUri = URI.joinPath(selectedFolder, fileName); + if (await fileService.exists(fileUri)) { + return { + content: localize('askForPromptFileName.error.exists', "A file for the given name already exists."), + severity: Severity.Error + }; + } + + return undefined; + }; + + const result = await quickInputService.input({ placeHolder, validateInput }); if (!result) { return undefined; } - const trimmedName = result.trim(); - if (!trimmedName) { - return undefined; - } - - const fileExtension = getPromptFileExtension(type); - const cleanName = (trimmedName.endsWith(fileExtension)) - ? trimmedName - : `${trimmedName}${fileExtension}`; - - return cleanName; + return sanitizeInput(result); }; diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index cacaa8bff22..f47b7ddf412 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -35,7 +35,9 @@ export class ChatRequestParser { parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel, context?: IChatParserContext): IParsedChatRequest { const parts: IParsedChatRequestPart[] = []; const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls - const toolsByName = new Map((this.variableService.getSelectedTools(sessionId)).filter(t => t.toolReferenceName).map(t => [t.toolReferenceName!, t])); + const toolsByName = new Map((this.variableService.getSelectedTools(sessionId)) + .filter(t => t.canBeReferencedInPrompt && t.toolReferenceName) + .map(t => [t.toolReferenceName!, t])); let lineNumber = 1; let column = 1; @@ -154,7 +156,7 @@ export class ChatRequestParser { const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); const tool = toolsByName.get(name); - if (tool && tool.canBeReferencedInPrompt) { + if (tool) { return new ChatRequestToolPart(varRange, varEditorRange, name, tool.id, tool.displayName, tool.icon); } diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index f854723499f..15c4f7d79b1 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -100,12 +100,12 @@ export function isToolInvocationContext(obj: any): obj is IToolInvocationContext export interface IToolResultInputOutputDetails { readonly input: string; - readonly output: string; + readonly output: ({ type: 'text'; value: string } | { type: 'data'; mimeType: string; value64: string })[]; readonly isError?: boolean; } export function isToolResultInputOutputDetails(obj: any): obj is IToolResultInputOutputDetails { - return typeof obj === 'object' && typeof obj?.input === 'string' && typeof obj?.output === 'string'; + return typeof obj === 'object' && typeof obj?.input === 'string' && (typeof obj?.output === 'string' || Array.isArray(obj?.output)); } export interface IToolResult { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/configMigration.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/configMigration.ts new file mode 100644 index 00000000000..1baac3ffc4d --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/configMigration.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assert } from '../../../../../../base/common/assert.js'; +import { asBoolean } from '../../../../../../platform/prompts/common/config.js'; +import { IWorkbenchContribution } from '../../../../../common/contributions.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { CONFIG_KEY, PROMPT_LOCATIONS_CONFIG_KEY } from '../../../../../../platform/prompts/common/constants.js'; + +/** + * Contribution that migrates the old config setting value to a new one. + * + * Note! This is a temporary logic and can be removed on ~2026-04-29. + */ +export class ConfigMigration implements IWorkbenchContribution { + constructor( + @IConfigurationService configService: IConfigurationService, + ) { + const value = configService.getValue(CONFIG_KEY); + + // if setting is not set, nothing to do + if ((value === undefined) || (value === null)) { + return; + } + + // if the setting value is a boolean, we don't need to do + // anything since it is already a valid configuration value + if ((typeof value === 'boolean') || (asBoolean(value) !== undefined)) { + return; + } + + // in the old setting logic an array of strings was treated + // as a list of locations, so we need to migrate that + if (Array.isArray(value)) { + + // copy array values into a map of paths + const locationsValue: Record = {}; + for (const filePath of value) { + if (typeof filePath !== 'string') { + continue; + } + const trimmedValue = filePath.trim(); + if (!trimmedValue) { + continue; + } + + locationsValue[trimmedValue] = true; + } + + configService.updateValue(CONFIG_KEY, true); + configService.updateValue(PROMPT_LOCATIONS_CONFIG_KEY, locationsValue); + return; + } + + // in the old setting logic an object was treated as a map + // of `location -> boolean`, so we need to migrate that + if (typeof value === 'object') { + // sanity check on the contents of value variable - while + // we've handled the 'null' case above this assertion is + // here to prevent churn when this block is moved around + assert( + value !== null, + 'Object value must not be a null.', + ); + + // copy object values into a map of paths + const locationsValue: Record = {}; + for (const [location, enabled] of Object.entries(value)) { + // if the old location enabled value wasn't a boolean + // then ignore it as it is not a valid value + if ((typeof enabled !== 'boolean') || (asBoolean(enabled) === undefined)) { + continue; + } + + const trimmedValue = location.trim(); + if (!trimmedValue) { + continue; + } + + locationsValue[trimmedValue] = enabled; + } + + configService.updateValue(CONFIG_KEY, true); + configService.updateValue(PROMPT_LOCATIONS_CONFIG_KEY, locationsValue); + + return; + } + + // in the old setting logic a string was treated as a single + // location path, so we need to migrate that + if (typeof value === 'string') { + // sanity check on the contents of value variable - while + // we've handled the 'boolean' case above this assertion is + // here to prevent churn when this block is moved around + assert( + asBoolean(value) === undefined, + `String value must not be a boolean, got '${value}'.`, + ); + + configService.updateValue(CONFIG_KEY, true); + configService.updateValue(PROMPT_LOCATIONS_CONFIG_KEY, { [value]: true }); + return; + } + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/index.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/index.ts new file mode 100644 index 00000000000..2eebfc02899 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/index.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 { ConfigMigration } from './configMigration.js'; +import { LANGUAGE_FEATURE_CONTRIBUTIONS } from './languageFeatures/index.js'; +import { Registry } from '../../../../../../platform/registry/common/platform.js'; +import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { IWorkbenchContributionsRegistry, Extensions, IWorkbenchContribution } from '../../../../../common/contributions.js'; + +/** + * Function that registers all prompt-file related contributions. + */ +export const registerPromptFileContributions = () => { + registerContributions(LANGUAGE_FEATURE_CONTRIBUTIONS); + + registerContribution(ConfigMigration); +}; + +/** + * Type for a generic workbench contribution. + */ +export type TContribution = new (...args: any[]) => IWorkbenchContribution; + +/** + * Register a specific workbench contribution. + */ +const registerContribution = ( + contribution: TContribution, +) => { + Registry.as(Extensions.Workbench) + .registerWorkbenchContribution(contribution, LifecyclePhase.Eventually); +}; + +/** + * Register a specific workbench contribution. + */ +const registerContributions = ( + contributions: readonly TContribution[], +) => { + contributions + .map(registerContribution); +}; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/index.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/index.ts new file mode 100644 index 00000000000..e296fdecd89 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/index.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TContribution } from '../index.js'; +import { PromptLinkProvider } from './providers/promptLinkProvider.js'; +import { isWindows } from '../../../../../../../base/common/platform.js'; +import { PromptPathAutocompletion } from './providers/promptPathAutocompletion.js'; +import { PromptLinkDiagnosticsInstanceManager } from './providers/promptLinkDiagnosticsProvider.js'; +import { PromptHeaderDiagnosticsInstanceManager } from './providers/promptHeaderDiagnosticsProvider.js'; +import { PromptDecorationsProviderInstanceManager } from './providers/decorationsProvider/promptDecorationsProvider.js'; + +/** + * Base list of language feature contributions. + */ +const CONTRIBUTIONS: TContribution[] = [ + PromptLinkProvider, + PromptLinkDiagnosticsInstanceManager, + PromptHeaderDiagnosticsInstanceManager, + PromptDecorationsProviderInstanceManager, +]; + +/** + * We restrict this provider to `Unix` machines for now because of + * the filesystem paths differences on `Windows` operating system. + * + * Notes on `Windows` support: + * - we add the `./` for the first path component, which may not work on `Windows` + * - the first path component of the absolute paths must be a drive letter + */ +if (isWindows === false) { + CONTRIBUTIONS.push(PromptPathAutocompletion); +} + +/** + * List of language feature contributions for the prompt files. + */ +export const LANGUAGE_FEATURE_CONTRIBUTIONS = Object.freeze(CONTRIBUTIONS); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts similarity index 88% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts index 7dc4458ca48..b2c0602dff4 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterDecoration.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { CssClassModifiers } from '../types.js'; -import { localize } from '../../../../../../../../../nls.js'; +import { localize } from '../../../../../../../../../../nls.js'; import { FrontMatterMarkerDecoration } from './frontMatterMarkerDecoration.js'; -import { Position } from '../../../../../../../../../editor/common/core/position.js'; -import { BaseToken } from '../../../../../../../../../editor/common/codecs/baseToken.js'; +import { Position } from '../../../../../../../../../../editor/common/core/position.js'; +import { BaseToken } from '../../../../../../../../../../editor/common/codecs/baseToken.js'; import { TAddAccessor, TDecorationStyles, ReactiveDecorationBase, asCssVariable } from './utils/index.js'; -import { contrastBorder, editorBackground } from '../../../../../../../../../platform/theme/common/colorRegistry.js'; -import { ColorIdentifier, darken, registerColor } from '../../../../../../../../../platform/theme/common/colorUtils.js'; -import { FrontMatterHeader } from '../../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; +import { contrastBorder, editorBackground } from '../../../../../../../../../../platform/theme/common/colorRegistry.js'; +import { ColorIdentifier, darken, registerColor } from '../../../../../../../../../../platform/theme/common/colorUtils.js'; +import { FrontMatterHeader } from '../../../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; /** * Decoration CSS class names. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/frontMatterMarkerDecoration.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterMarkerDecoration.ts similarity index 92% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/frontMatterMarkerDecoration.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterMarkerDecoration.ts index 6955598cf06..31ad42e50b4 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/frontMatterMarkerDecoration.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/frontMatterMarkerDecoration.ts @@ -5,7 +5,7 @@ import { CssClassModifiers } from '../types.js'; import { TDecorationStyles, ReactiveDecorationBase } from './utils/index.js'; -import { FrontMatterMarker } from '../../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.js'; +import { FrontMatterMarker } from '../../../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.js'; /** * Decoration CSS class names. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/decorationBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/decorationBase.ts similarity index 89% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/decorationBase.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/decorationBase.ts index feaafbe966d..614a22a7ce9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/decorationBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/decorationBase.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Range } from '../../../../../../../../../../editor/common/core/range.js'; -import { IMarkdownString } from '../../../../../../../../../../base/common/htmlContent.js'; -import { BaseToken } from '../../../../../../../../../../editor/common/codecs/baseToken.js'; -import { TrackedRangeStickiness } from '../../../../../../../../../../editor/common/model.js'; +import { Range } from '../../../../../../../../../../../editor/common/core/range.js'; +import { IMarkdownString } from '../../../../../../../../../../../base/common/htmlContent.js'; +import { BaseToken } from '../../../../../../../../../../../editor/common/codecs/baseToken.js'; +import { TrackedRangeStickiness } from '../../../../../../../../../../../editor/common/model.js'; import type { TAddAccessor, TChangeAccessor, TDecorationStyles, TRemoveAccessor } from './types.js'; -import { ModelDecorationOptions } from '../../../../../../../../../../editor/common/model/textModel.js'; +import { ModelDecorationOptions } from '../../../../../../../../../../../editor/common/model/textModel.js'; /** * Base class for all editor decorations. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/index.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/index.ts similarity index 87% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/index.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/index.ts index bfe3e55bb19..3980294fc1a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/index.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/index.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ColorIdentifier } from '../../../../../../../../../../platform/theme/common/colorUtils.js'; +import { ColorIdentifier } from '../../../../../../../../../../../platform/theme/common/colorUtils.js'; /** * Convert a registered color to a CSS variable string. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/reactiveDecorationBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/reactiveDecorationBase.ts similarity index 95% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/reactiveDecorationBase.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/reactiveDecorationBase.ts index d36262fc18c..2d07e97574b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/reactiveDecorationBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/reactiveDecorationBase.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { DecorationBase } from './decorationBase.js'; -import { Position } from '../../../../../../../../../../editor/common/core/position.js'; -import { BaseToken } from '../../../../../../../../../../editor/common/codecs/baseToken.js'; +import { Position } from '../../../../../../../../../../../editor/common/core/position.js'; +import { BaseToken } from '../../../../../../../../../../../editor/common/codecs/baseToken.js'; import type { IReactiveDecorationClassNames, TAddAccessor, TChangeAccessor, TRemoveAccessor } from './types.js'; /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/types.d.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/types.d.ts similarity index 96% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/types.d.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/types.d.ts index a24ad4b2162..73086f0e860 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/decorations/utils/types.d.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/decorations/utils/types.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IModelDecorationsChangeAccessor, TrackedRangeStickiness } from '../../../../../../../../../../editor/common/model.js'; +import { IModelDecorationsChangeAccessor, TrackedRangeStickiness } from '../../../../../../../../../../../editor/common/model.ts'; /** * CSS class names of a `reactive` decoration. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts similarity index 89% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts index 65bf1dcc328..45bfbe355ff 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/promptDecorationsProvider.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IPromptsService } from '../../../service/types.js'; +import { IPromptsService } from '../../../../service/types.js'; import { ProviderInstanceBase } from '../providerInstanceBase.js'; -import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { ITextModel } from '../../../../../../../../../editor/common/model.js'; import { FrontMatterDecoration } from './decorations/frontMatterDecoration.js'; -import { toDisposable } from '../../../../../../../../base/common/lifecycle.js'; +import { toDisposable } from '../../../../../../../../../base/common/lifecycle.js'; import { ProviderInstanceManagerBase } from '../providerInstanceManagerBase.js'; -import { Position } from '../../../../../../../../editor/common/core/position.js'; -import { BaseToken } from '../../../../../../../../editor/common/codecs/baseToken.js'; -import { registerThemingParticipant } from '../../../../../../../../platform/theme/common/themeService.js'; -import { FrontMatterHeader } from '../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; +import { Position } from '../../../../../../../../../editor/common/core/position.js'; +import { BaseToken } from '../../../../../../../../../editor/common/codecs/baseToken.js'; +import { registerThemingParticipant } from '../../../../../../../../../platform/theme/common/themeService.js'; +import { FrontMatterHeader } from '../../../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; import { DecorationBase, ReactiveDecorationBase, type TDecorationClass, type TChangedDecorator } from './decorations/utils/index.js'; /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/types.ts similarity index 90% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/types.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/types.ts index a7ef26521d2..f1634345088 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/decorationsProvider/types.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/decorationsProvider/types.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRange } from '../../../../../../../../editor/common/core/range.js'; -import { ModelDecorationOptions } from '../../../../../../../../editor/common/model/textModel.js'; +import { IRange } from '../../../../../../../../../editor/common/core/range.js'; +import { ModelDecorationOptions } from '../../../../../../../../../editor/common/model/textModel.js'; /** * Decoration object. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts similarity index 89% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts index db6dbce4cb7..e3ab9e55604 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptHeaderDiagnosticsProvider.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IPromptsService } from '../../service/types.js'; +import { IPromptsService } from '../../../service/types.js'; import { ProviderInstanceBase } from './providerInstanceBase.js'; -import { assertNever } from '../../../../../../../base/common/assert.js'; -import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { assertNever } from '../../../../../../../../base/common/assert.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; import { ProviderInstanceManagerBase } from './providerInstanceManagerBase.js'; -import { TDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../../parsers/promptHeader/diagnostics.js'; -import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../../platform/markers/common/markers.js'; +import { TDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../../../parsers/promptHeader/diagnostics.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../../../platform/markers/common/markers.js'; /** * Unique ID of the markers provider class. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkDiagnosticsProvider.ts similarity index 87% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkDiagnosticsProvider.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkDiagnosticsProvider.ts index 29400894fa1..8364bc28e66 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkDiagnosticsProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkDiagnosticsProvider.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IPromptsService } from '../../service/types.js'; -import { IPromptFileReference } from '../../parsers/types.js'; +import { IPromptsService } from '../../../service/types.js'; +import { IPromptFileReference } from '../../../parsers/types.js'; import { ProviderInstanceBase } from './providerInstanceBase.js'; -import { assert } from '../../../../../../../base/common/assert.js'; -import { NotPromptFile } from '../../../promptFileReferenceErrors.js'; -import { ITextModel } from '../../../../../../../editor/common/model.js'; -import { assertDefined } from '../../../../../../../base/common/types.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { NotPromptFile } from '../../../../promptFileReferenceErrors.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; import { ProviderInstanceManagerBase } from './providerInstanceManagerBase.js'; -import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../../platform/markers/common/markers.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../../../platform/markers/common/markers.js'; /** * Unique ID of the markers provider class. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts similarity index 74% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts index 362126eb3d9..8538999ff11 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptLinkProvider.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IPromptsService } from '../../service/types.js'; -import { assert } from '../../../../../../../base/common/assert.js'; -import { ITextModel } from '../../../../../../../editor/common/model.js'; -import { assertDefined } from '../../../../../../../base/common/types.js'; -import { Disposable } from '../../../../../../../base/common/lifecycle.js'; -import { CancellationError } from '../../../../../../../base/common/errors.js'; -import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../constants.js'; -import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; -import { FolderReference, NotPromptFile } from '../../../promptFileReferenceErrors.js'; -import { ILink, ILinksList, LinkProvider } from '../../../../../../../editor/common/languages.js'; -import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'; +import { IPromptsService } from '../../../service/types.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { CancellationError } from '../../../../../../../../base/common/errors.js'; +import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../../constants.js'; +import { CancellationToken } from '../../../../../../../../base/common/cancellation.js'; +import { FolderReference, NotPromptFile } from '../../../../promptFileReferenceErrors.js'; +import { ILink, ILinksList, LinkProvider } from '../../../../../../../../editor/common/languages.js'; +import { ILanguageFeaturesService } from '../../../../../../../../editor/common/services/languageFeatures.js'; /** * Provides link references for prompt files. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts similarity index 90% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts index 1a3dc385651..a2c8b946b28 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/promptPathAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/promptPathAutocompletion.ts @@ -14,21 +14,21 @@ * - add `Windows` support */ -import { IPromptsService } from '../../service/types.js'; -import { URI } from '../../../../../../../base/common/uri.js'; -import { extUri } from '../../../../../../../base/common/resources.js'; -import { assertOneOf } from '../../../../../../../base/common/types.js'; -import { ITextModel } from '../../../../../../../editor/common/model.js'; -import { Disposable } from '../../../../../../../base/common/lifecycle.js'; -import { CancellationError } from '../../../../../../../base/common/errors.js'; -import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../constants.js'; -import { Position } from '../../../../../../../editor/common/core/position.js'; -import { IPromptFileReference, IPromptReference } from '../../parsers/types.js'; -import { assert, assertNever } from '../../../../../../../base/common/assert.js'; -import { IFileService } from '../../../../../../../platform/files/common/files.js'; -import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; -import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'; -import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../../../editor/common/languages.js'; +import { IPromptsService } from '../../../service/types.js'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { extUri } from '../../../../../../../../base/common/resources.js'; +import { assertOneOf } from '../../../../../../../../base/common/types.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { CancellationError } from '../../../../../../../../base/common/errors.js'; +import { PROMPT_AND_INSTRUCTIONS_LANGUAGE_SELECTOR } from '../../../constants.js'; +import { Position } from '../../../../../../../../editor/common/core/position.js'; +import { IPromptFileReference, IPromptReference } from '../../../parsers/types.js'; +import { assert, assertNever } from '../../../../../../../../base/common/assert.js'; +import { IFileService } from '../../../../../../../../platform/files/common/files.js'; +import { CancellationToken } from '../../../../../../../../base/common/cancellation.js'; +import { ILanguageFeaturesService } from '../../../../../../../../editor/common/services/languageFeatures.js'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../../../../editor/common/languages.js'; /** * Type for a filesystem completion item - the one that has its {@link CompletionItem.kind kind} set diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceBase.ts similarity index 82% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceBase.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceBase.ts index 2cb0b49b7b1..7e812259615 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceBase.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IPromptsService, TSharedPrompt } from '../../service/types.js'; -import { ITextModel } from '../../../../../../../editor/common/model.js'; -import { ObservableDisposable } from '../../../../../../../base/common/observableDisposable.js'; +import { IPromptsService, TSharedPrompt } from '../../../service/types.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { ObservableDisposable } from '../../../../../../../../base/common/observableDisposable.js'; /** * Abstract base class for all reusable prompt file providers. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts similarity index 83% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts index 9f302ec3ef4..c766123e204 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/providerInstanceManagerBase.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/contributions/languageFeatures/providers/providerInstanceManagerBase.ts @@ -4,18 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { ProviderInstanceBase } from './providerInstanceBase.js'; -import { assert } from '../../../../../../../base/common/assert.js'; -import { ITextModel } from '../../../../../../../editor/common/model.js'; -import { assertDefined } from '../../../../../../../base/common/types.js'; -import { Disposable } from '../../../../../../../base/common/lifecycle.js'; -import { ObjectCache } from '../../../../../../../base/common/objectCache.js'; -import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../constants.js'; -import { IModelService } from '../../../../../../../editor/common/services/model.js'; -import { PromptsConfig } from '../../../../../../../platform/prompts/common/config.js'; -import { IEditorService } from '../../../../../../services/editor/common/editorService.js'; -import { IDiffEditor, IEditor, IEditorModel } from '../../../../../../../editor/common/editorCommon.js'; -import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; -import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { assert } from '../../../../../../../../base/common/assert.js'; +import { ITextModel } from '../../../../../../../../editor/common/model.js'; +import { assertDefined } from '../../../../../../../../base/common/types.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { ObjectCache } from '../../../../../../../../base/common/objectCache.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../constants.js'; +import { IModelService } from '../../../../../../../../editor/common/services/model.js'; +import { PromptsConfig } from '../../../../../../../../platform/prompts/common/config.js'; +import { IEditorService } from '../../../../../../../services/editor/common/editorService.js'; +import { IDiffEditor, IEditor, IEditorModel } from '../../../../../../../../editor/common/editorCommon.js'; +import { IInstantiationService } from '../../../../../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../../../../../platform/configuration/common/configuration.js'; /** * Type for a text editor that is used for reusable prompt files. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts deleted file mode 100644 index 5630fb95e60..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/providers/index.ts +++ /dev/null @@ -1,55 +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 { PromptLinkProvider } from './promptLinkProvider.js'; -import { isWindows } from '../../../../../../../base/common/platform.js'; -import { PromptPathAutocompletion } from './promptPathAutocompletion.js'; -import { Registry } from '../../../../../../../platform/registry/common/platform.js'; -import { LifecyclePhase } from '../../../../../../services/lifecycle/common/lifecycle.js'; -import { PromptLinkDiagnosticsInstanceManager } from './promptLinkDiagnosticsProvider.js'; -import { PromptHeaderDiagnosticsInstanceManager } from './promptHeaderDiagnosticsProvider.js'; -import { BrandedService } from '../../../../../../../platform/instantiation/common/instantiation.js'; -import { PromptDecorationsProviderInstanceManager } from './decorationsProvider/promptDecorationsProvider.js'; -import { IWorkbenchContributionsRegistry, Extensions, IWorkbenchContribution } from '../../../../../../common/contributions.js'; - -/** - * Whether to enable decorations in the prompt editor. - */ -export const DECORATIONS_ENABLED = true; - -/** - * Register all language features related to reusable prompts files. - */ -export const registerReusablePromptLanguageFeatures = () => { - registerContribution(PromptLinkProvider); - registerContribution(PromptLinkDiagnosticsInstanceManager); - registerContribution(PromptHeaderDiagnosticsInstanceManager); - - if (DECORATIONS_ENABLED) { - registerContribution(PromptDecorationsProviderInstanceManager); - } - - /** - * We restrict this provider to `Unix` machines for now because of - * the filesystem paths differences on `Windows` operating system. - * - * Notes on `Windows` support: - * - we add the `./` for the first path component, which may not work on `Windows` - * - the first path component of the absolute paths must be a drive letter - */ - if (isWindows === false) { - registerContribution(PromptPathAutocompletion); - } -}; - -/** - * Register a specific workbench contribution. - */ -const registerContribution = ( - contribution: new (...services: TServices) => IWorkbenchContribution, -) => { - Registry.as(Extensions.Workbench) - .registerWorkbenchContribution(contribution, LifecyclePhase.Eventually); -}; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts index aa76793b1ce..f1302c9fcae 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts @@ -8,6 +8,7 @@ import { ChatMode } from '../../constants.js'; import { PromptHeader } from './promptHeader/header.js'; import { URI } from '../../../../../../base/common/uri.js'; import { PromptToken } from '../codecs/tokens/promptToken.js'; +import * as path from '../../../../../../base/common/path.js'; import { ChatPromptCodec } from '../codecs/chatPromptCodec.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { FileReference } from '../codecs/tokens/fileReference.js'; @@ -19,9 +20,9 @@ import { ILogService } from '../../../../../../platform/log/common/log.js'; import { PromptVariableWithData } from '../codecs/tokens/promptVariable.js'; import { IRange, Range } from '../../../../../../editor/common/core/range.js'; import { assert, assertNever } from '../../../../../../base/common/assert.js'; +import { basename, dirname } from '../../../../../../base/common/resources.js'; import { BaseToken } from '../../../../../../editor/common/codecs/baseToken.js'; import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; -import { basename, dirname, extUri } from '../../../../../../base/common/resources.js'; import { IPromptMetadata, IPromptReference, IResolveError, ITopError } from './types.js'; import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; @@ -360,8 +361,8 @@ export class BasePromptParser ): this { const { parentFolder } = this; - const referenceUri = (parentFolder !== null) - ? extUri.resolvePath(parentFolder, token.path) + const referenceUri = ((parentFolder !== null) && (path.isAbsolute(token.path) === false)) + ? URI.joinPath(parentFolder, token.path) : URI.file(token.path); const contentProvider = this.promptContentsProvider.createNew({ uri: referenceUri }); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index ed14dc52fc1..e5768b7fca8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -209,7 +209,7 @@ export class PromptsService extends Disposable implements IPromptsService { } } - return result; + return [...new Set(result)]; } public async getAllMetadata( 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 6dd2e47985a..825f843715f 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -546,7 +546,7 @@ const primaryVoiceActionMenu = (when: ContextKeyExpression | undefined) => { id: MenuId.ChatInput, when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), when), group: 'navigation', - order: 3 + order: 0 }, { id: MenuId.ChatExecute, diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts index 46573d18f65..7eadafaefb6 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/markdownExtensionsDecoder.test.ts @@ -17,8 +17,8 @@ import { NewLine } from '../../../../../../../editor/common/codecs/linesCodec/to import { TestSimpleDecoder } from '../../../../../../../editor/test/common/codecs/simpleDecoder.test.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { CarriageReturn } from '../../../../../../../editor/common/codecs/linesCodec/tokens/carriageReturn.js'; -import { Colon, Dash, Space, Tab, VerticalTab } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; import { FrontMatterHeader } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterHeader.js'; +import { Colon, Dash, DoubleQuote, Space, Tab, VerticalTab } from '../../../../../../../editor/common/codecs/simpleCodec/tokens/index.js'; import { MarkdownExtensionsDecoder } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/markdownExtensionsDecoder.js'; import { FrontMatterMarker, TMarkerToken } from '../../../../../../../editor/common/codecs/markdownExtensionsCodec/tokens/frontMatterMarker.js'; @@ -102,7 +102,7 @@ class TestFrontMatterMarker extends FrontMatterMarker { public static create( dashCount: number, lineNumber: number, - endOfLine: TEndOfLine, + endOfLine?: TEndOfLine | undefined, ): TestFrontMatterMarker { const tokens: TMarkerToken[] = []; @@ -120,12 +120,14 @@ class TestFrontMatterMarker extends FrontMatterMarker { columnNumber++; } - const endOfLineTokens = TestEndOfLine.create( - endOfLine, - lineNumber, - columnNumber, - ); - tokens.push(...endOfLineTokens.tokens); + if (endOfLine !== undefined) { + const endOfLineTokens = TestEndOfLine.create( + endOfLine, + lineNumber, + columnNumber, + ); + tokens.push(...endOfLineTokens.tokens); + } return TestFrontMatterMarker.fromTokens(tokens); } @@ -273,6 +275,55 @@ suite('MarkdownExtensionsDecoder', () => { ], ); }); + + test('• can be at the end of the file', async () => { + const test = disposables.add( + new TestMarkdownExtensionsDecoder(), + ); + + // both line endings should result in the same result + const newLine = (randomBoolean()) + ? '\n' + : '\r\n'; + + const markerLength = randomInt(10, 4); + + const promptContents = [ + // start marker + new Array(markerLength).fill('-').join(''), + // contents + ' description: "my description"', + // end marker + new Array(markerLength).fill('-').join(''), + ]; + + const startMarker = TestFrontMatterMarker.create(markerLength, 1, newLine); + const endMarker = TestFrontMatterMarker.create(markerLength, 3); + + await test.run( + promptContents.join(newLine), + [ + // header + new FrontMatterHeader( + new Range(1, 1, 3, 1 + markerLength), + startMarker, + Text.fromTokens([ + new Tab(new Range(2, 1, 2, 2)), + new Word(new Range(2, 2, 2, 2 + 11), 'description'), + new Colon(new Range(2, 13, 2, 14)), + new Space(new Range(2, 14, 2, 15)), + new DoubleQuote(new Range(2, 15, 2, 16)), + new Word(new Range(2, 16, 2, 16 + 2), 'my'), + new Space(new Range(2, 18, 2, 19)), + new Word(new Range(2, 19, 2, 19 + 11), 'description'), + new DoubleQuote(new Range(2, 30, 2, 31)), + ...TestEndOfLine.create(newLine, 2, 31).tokens, + ]), + endMarker, + ), + ], + ); + }); }); suite('• failure cases', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts index aa3d01d2978..d5735551662 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/parsers/textModelPromptParser.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { createURI } from '../testUtils/createUri.js'; import { ChatMode } from '../../../../common/constants.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { Schemas } from '../../../../../../../base/common/network.js'; @@ -179,7 +178,7 @@ suite('TextModelPromptParser', () => { test('• core logic #1', async () => { const test = createTest( - createURI('/foo/bar.md'), + URI.file('/foo/bar.md'), [ /* 01 */"The quick brown fox tries #file:/abs/path/to/file.md online yoga for the first time.", /* 02 */"Maria discovered a stray turtle roaming in her kitchen.", @@ -198,38 +197,38 @@ suite('TextModelPromptParser', () => { await test.validateReferences([ new ExpectedReference({ - uri: createURI('/abs/path/to/file.md'), + uri: URI.file('/abs/path/to/file.md'), text: '#file:/abs/path/to/file.md', path: '/abs/path/to/file.md', startLine: 1, startColumn: 27, pathStartColumn: 33, - childrenOrError: new OpenFailed(createURI('/abs/path/to/file.md'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/abs/path/to/file.md'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/foo/folder/binary.file'), + uri: URI.file('/foo/folder/binary.file'), text: '#file:./folder/binary.file', path: './folder/binary.file', startLine: 7, startColumn: 10, pathStartColumn: 16, - childrenOrError: new OpenFailed(createURI('/foo/folder/binary.file'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/foo/folder/binary.file'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/etc/hosts/random-file.txt'), + uri: URI.file('/etc/hosts/random-file.txt'), text: '[md link](/etc/hosts/random-file.txt)', path: '/etc/hosts/random-file.txt', startLine: 7, startColumn: 81, pathStartColumn: 91, - childrenOrError: new OpenFailed(createURI('/etc/hosts/random-file.txt'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/etc/hosts/random-file.txt'), 'File not found.'), }), ]); }); test('• core logic #2', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"The penguin wore sunglasses but never left the iceberg.", /* 02 */"I once saw a cloud that looked like an antique teapot.", @@ -251,40 +250,40 @@ suite('TextModelPromptParser', () => { await test.validateReferences([ new ExpectedReference({ - uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + uri: URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), text: '[link text](./foo-bar-baz/another-file.ts)', path: './foo-bar-baz/another-file.ts', startLine: 3, startColumn: 43, pathStartColumn: 55, - childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/absolute/c/file_name.prompt.md'), + uri: URI.file('/absolute/c/file_name.prompt.md'), text: '[caption](../../../c/file_name.prompt.md)', path: '../../../c/file_name.prompt.md', startLine: 6, startColumn: 7, pathStartColumn: 17, - childrenOrError: new OpenFailed(createURI('/absolute/c/file_name.prompt.md'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/c/file_name.prompt.md'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/absolute/folder/main.rs'), + uri: URI.file('/absolute/folder/main.rs'), text: '#file:../../main.rs', path: '../../main.rs', startLine: 11, startColumn: 36, pathStartColumn: 42, - childrenOrError: new OpenFailed(createURI('/absolute/folder/main.rs'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/folder/main.rs'), 'File not found.'), }), new ExpectedReference({ - uri: createURI('/absolute/folder/and/a/samefile.jpeg'), + uri: URI.file('/absolute/folder/and/a/samefile.jpeg'), text: '#file:./somefolder/../samefile.jpeg', path: './somefolder/../samefile.jpeg', startLine: 11, startColumn: 56, pathStartColumn: 62, - childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/samefile.jpeg'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/samefile.jpeg'), 'File not found.'), }), ]); }); @@ -293,7 +292,7 @@ suite('TextModelPromptParser', () => { suite(' • metadata', () => { test('• has correct \'prompt\' metadata', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */"description: 'My prompt.'\t\t", @@ -314,13 +313,13 @@ suite('TextModelPromptParser', () => { await test.validateReferences([ new ExpectedReference({ - uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + uri: URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), text: '[text](./foo-bar-baz/another-file.ts)', path: './foo-bar-baz/another-file.ts', startLine: 11, startColumn: 43, pathStartColumn: 50, - childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), }), ]); @@ -358,7 +357,7 @@ suite('TextModelPromptParser', () => { test('• has correct \'instructions\' metadata', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.instructions.md'), + URI.file('/absolute/folder/and/a/filename.instructions.md'), [ /* 01 */"---", /* 02 */"description: 'My prompt.'\t\t", @@ -380,13 +379,13 @@ suite('TextModelPromptParser', () => { await test.validateReferences([ new ExpectedReference({ - uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + uri: URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), text: '[text](./foo-bar-baz/another-file.ts)', path: './foo-bar-baz/another-file.ts', startLine: 11, startColumn: 43, pathStartColumn: 50, - childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), }), ]); @@ -426,7 +425,7 @@ suite('TextModelPromptParser', () => { suite('• diagnostics', () => { test('• core logic', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */" description: true \t ", @@ -446,13 +445,13 @@ suite('TextModelPromptParser', () => { await test.validateReferences([ new ExpectedReference({ - uri: createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), + uri: URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), text: '[text](./foo-bar-baz/another-file.ts)', path: './foo-bar-baz/another-file.ts', startLine: 10, startColumn: 43, pathStartColumn: 50, - childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), + childrenOrError: new OpenFailed(URI.file('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'), }), ]); @@ -516,7 +515,7 @@ suite('TextModelPromptParser', () => { suite('• language', () => { test('• prompt', async () => { const test = createTest( - createURI('/absolute/folder/and/a/my.prompt.md'), + URI.file('/absolute/folder/and/a/my.prompt.md'), [ /* 01 */"---", /* 02 */"applyTo: '**/*'", @@ -557,7 +556,7 @@ suite('TextModelPromptParser', () => { test('• instructions', async () => { const test = createTest( - createURI('/absolute/folder/and/a/my.prompt.md'), + URI.file('/absolute/folder/and/a/my.prompt.md'), [ /* 01 */"---", /* 02 */"applyTo: '**/*'", @@ -596,7 +595,7 @@ suite('TextModelPromptParser', () => { test('• invalid glob pattern', async () => { const test = createTest( - createURI('/absolute/folder/and/a/my.prompt.md'), + URI.file('/absolute/folder/and/a/my.prompt.md'), [ /* 01 */"---", /* 02 */"mode: \"agent\"", @@ -640,7 +639,7 @@ suite('TextModelPromptParser', () => { suite('• tools is set', () => { test('• ask mode', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ @@ -680,7 +679,7 @@ suite('TextModelPromptParser', () => { test('• edit mode', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ @@ -720,7 +719,7 @@ suite('TextModelPromptParser', () => { test('• agent mode', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ @@ -755,7 +754,7 @@ suite('TextModelPromptParser', () => { test('• no mode', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */"tools: [ 'tool_name3', \"tool_name4\" ] \t\t ", /* duplicate `tools` record is ignored */ @@ -791,7 +790,7 @@ suite('TextModelPromptParser', () => { suite('• tools is not set', () => { test('• ask mode', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */"description: ['my prompt', 'description.']", @@ -831,7 +830,7 @@ suite('TextModelPromptParser', () => { test('• edit mode', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */"description: my prompt description. \t\t \t\t ", @@ -887,7 +886,7 @@ suite('TextModelPromptParser', () => { test('• agent mode', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */"mode: \"agent\"", @@ -921,7 +920,7 @@ suite('TextModelPromptParser', () => { test('• no mode', async () => { const test = createTest( - createURI('/absolute/folder/and/a/filename.txt'), + URI.file('/absolute/folder/and/a/filename.txt'), [ /* 01 */"---", /* 02 */"description: 'My prompt.'", @@ -959,7 +958,7 @@ suite('TextModelPromptParser', () => { test('• gets disposed with the model', async () => { const test = createTest( - createURI('/some/path/file.prompt.md'), + URI.file('/some/path/file.prompt.md'), [ 'line1', 'line2', @@ -979,7 +978,7 @@ suite('TextModelPromptParser', () => { }); test('• toString() implementation', async () => { - const modelUri = createURI('/Users/legomushroom/repos/prompt-snippets/README.md'); + const modelUri = URI.file('/Users/legomushroom/repos/prompt-snippets/README.md'); const test = createTest( modelUri, [ diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts index 014aa1b857f..f4fd4556e9a 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileReference.test.ts @@ -7,8 +7,6 @@ import assert from 'assert'; import { ChatMode } from '../../../common/constants.js'; import { URI } from '../../../../../../base/common/uri.js'; import { Schemas } from '../../../../../../base/common/network.js'; -import { extUri } from '../../../../../../base/common/resources.js'; -import { isWindows } from '../../../../../../base/common/platform.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { assertDefined } from '../../../../../../base/common/types.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; @@ -50,7 +48,9 @@ class ExpectedReference { public readonly linkToken: FileReference | MarkdownLink, public readonly errorCondition?: TErrorCondition, ) { - this.uri = extUri.resolvePath(dirname, linkToken.path); + this.uri = (linkToken.path.startsWith('/')) + ? URI.file(linkToken.path) + : URI.joinPath(dirname, linkToken.path); } /** @@ -215,7 +215,7 @@ const createTestFileReference = ( return new FileReference(range, filePath); }; -suite('PromptFileReference (Unix)', function () { +suite('PromptFileReference', function () { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); let instantiationService: TestInstantiationService; @@ -251,10 +251,6 @@ suite('PromptFileReference (Unix)', function () { }); test('• resolves nested file references', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -388,14 +384,13 @@ suite('PromptFileReference (Unix)', function () { ), ), new ExpectedReference( - URI.joinPath(rootUri, './some-other-folder/folder1'), - // createTestFileReference('../../folder1', 5, 48), + URI.joinPath(rootUri, './folder1/some-other-folder'), new MarkdownLink( 5, 48, '[]', '(../../folder1/)', ), new FolderReference( - URI.joinPath(rootUri, './folder1'), + URI.joinPath(rootUri, './folder1/'), 'Uggh ohh!', ), ), @@ -406,10 +401,6 @@ suite('PromptFileReference (Unix)', function () { }); test('• does not fall into infinite reference recursion', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'infinite-recursion'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -560,10 +551,6 @@ suite('PromptFileReference (Unix)', function () { suite('• options', () => { test('• allowNonPromptFiles', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -689,7 +676,7 @@ suite('PromptFileReference (Unix)', function () { ), ), new ExpectedReference( - URI.joinPath(rootUri, './folder1/some-other-folder'), + URI.joinPath(rootUri, './folder1/some-other-folder/'), createTestFileReference('./some-non-prompt-file.md', 5, 13), new OpenFailed( URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.md'), @@ -697,7 +684,7 @@ suite('PromptFileReference (Unix)', function () { ), ), new ExpectedReference( - URI.joinPath(rootUri, './some-other-folder/folder1'), + URI.joinPath(rootUri, './some-other-folder/folder1/'), new MarkdownLink( 5, 48, '[]', '(../../folder1/)', @@ -716,10 +703,6 @@ suite('PromptFileReference (Unix)', function () { suite('• metadata', () => { test('• tools', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -934,10 +917,6 @@ suite('PromptFileReference (Unix)', function () { suite('• applyTo', () => { test('• prompt language', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -1070,10 +1049,6 @@ suite('PromptFileReference (Unix)', function () { test('• instructions language', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -1207,10 +1182,6 @@ suite('PromptFileReference (Unix)', function () { suite('• tools and mode compatibility', () => { test('• tools are ignored if root prompt in the ask mode', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -1331,10 +1302,6 @@ suite('PromptFileReference (Unix)', function () { }); test('• tools are ignored if root prompt in the edit mode', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -1454,10 +1421,6 @@ suite('PromptFileReference (Unix)', function () { }); test('• tools are not ignored if root prompt in the agent mode', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); @@ -1581,10 +1544,6 @@ suite('PromptFileReference (Unix)', function () { }); test('• tools are not ignored if root prompt implicitly in the agent mode', async function () { - if (isWindows) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; const rootUri = URI.file(rootFolder); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 013ce456125..124258a287f 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -5,7 +5,6 @@ import assert from 'assert'; import * as sinon from 'sinon'; -import { createURI } from '../testUtils/createUri.js'; import { ChatMode } from '../../../../common/constants.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { MockFilesystem } from '../testUtils/mockFilesystem.js'; @@ -22,7 +21,6 @@ import { PromptsService } from '../../../../common/promptSyntax/service/promptsS import { ILanguageService } from '../../../../../../../editor/common/languages/language.js'; import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; import { randomBoolean, waitRandom } from '../../../../../../../base/test/common/testUtils.js'; -import { isWindows, isNative, isElectron } from '../../../../../../../base/common/platform.js'; import { TextModelPromptParser } from '../../../../common/promptSyntax/parsers/textModelPromptParser.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; @@ -148,7 +146,7 @@ suite('PromptsService', () => { 'test1\n\t#file:./file.md\n\n\n [bin file](/root/tmp.bin)\t\n', languageId, undefined, - createURI('/Users/vscode/repos/test/file1.txt'), + URI.file('/Users/vscode/repos/test/file1.txt'), )); const parser1 = service.getSyntaxParserFor(model1); @@ -177,12 +175,12 @@ suite('PromptsService', () => { parser1.allReferences, [ new ExpectedLink( - createURI('/Users/vscode/repos/test/file.md'), + URI.file('/Users/vscode/repos/test/file.md'), new Range(2, 2, 2, 2 + 15), new Range(2, 8, 2, 8 + 9), ), new ExpectedLink( - createURI('/root/tmp.bin'), + URI.file('/root/tmp.bin'), new Range(5, 4, 5, 4 + 25), new Range(5, 15, 5, 15 + 13), ), @@ -219,7 +217,7 @@ suite('PromptsService', () => { 'some text #file:/absolute/path.txt \t\ntest-text2', languageId, undefined, - createURI('/Users/vscode/repos/test/some-folder/file.md'), + URI.file('/Users/vscode/repos/test/some-folder/file.md'), )); // wait for some random amount of time @@ -274,7 +272,7 @@ suite('PromptsService', () => { parser2.allReferences, [ new ExpectedLink( - createURI('/absolute/path.txt'), + URI.file('/absolute/path.txt'), new Range(1, 11, 1, 11 + 24), new Range(1, 17, 1, 17 + 18), ), @@ -293,12 +291,12 @@ suite('PromptsService', () => { parser1_1.allReferences, [ new ExpectedLink( - createURI('/Users/vscode/repos/test/file.md'), + URI.file('/Users/vscode/repos/test/file.md'), new Range(2, 2, 2, 2 + 15), new Range(2, 8, 2, 8 + 9), ), new ExpectedLink( - createURI('/root/tmp.bin'), + URI.file('/root/tmp.bin'), new Range(5, 4, 5, 4 + 25), new Range(5, 15, 5, 15 + 13), ), @@ -365,12 +363,12 @@ suite('PromptsService', () => { parser1_2.allReferences, [ new ExpectedLink( - createURI('/Users/vscode/repos/test/file.md'), + URI.file('/Users/vscode/repos/test/file.md'), new Range(2, 2, 2, 2 + 15), new Range(2, 8, 2, 8 + 9), ), new ExpectedLink( - createURI('/root/tmp.bin'), + URI.file('/root/tmp.bin'), new Range(5, 4, 5, 4 + 25), new Range(5, 15, 5, 15 + 13), ), @@ -412,7 +410,7 @@ suite('PromptsService', () => { 'some text #file:/absolute/path.txt \n [caption](.copilot/prompts/test.prompt.md)\t\n\t\n more text', languageId, undefined, - createURI('/Users/vscode/repos/test/some-folder/file.md'), + URI.file('/Users/vscode/repos/test/some-folder/file.md'), )); const parser2_1 = service.getSyntaxParserFor(model2_1); @@ -445,13 +443,13 @@ suite('PromptsService', () => { [ // the first link didn't change new ExpectedLink( - createURI('/absolute/path.txt'), + URI.file('/absolute/path.txt'), new Range(1, 11, 1, 11 + 24), new Range(1, 17, 1, 17 + 18), ), // the second link is new new ExpectedLink( - createURI('/Users/vscode/repos/test/some-folder/.copilot/prompts/test.prompt.md'), + URI.file('/Users/vscode/repos/test/some-folder/.copilot/prompts/test.prompt.md'), new Range(2, 2, 2, 2 + 42), new Range(2, 12, 2, 12 + 31), ), @@ -466,7 +464,7 @@ suite('PromptsService', () => { ' \t #file:../file.md\ntest1\n\t\n [another file](/Users/root/tmp/file2.txt)\t\n', langId, undefined, - createURI('/repos/test/file1.txt'), + URI.file('/repos/test/file1.txt'), )); const parser = service.getSyntaxParserFor(model); @@ -487,12 +485,12 @@ suite('PromptsService', () => { parser.allReferences, [ new ExpectedLink( - createURI('/repos/file.md'), + URI.file('/repos/file.md'), new Range(1, 4, 1, 4 + 16), new Range(1, 10, 1, 10 + 10), ), new ExpectedLink( - createURI('/Users/root/tmp/file2.txt'), + URI.file('/Users/root/tmp/file2.txt'), new Range(4, 3, 4, 3 + 41), new Range(4, 18, 4, 18 + 25), ), @@ -513,13 +511,13 @@ suite('PromptsService', () => { [ // link1 didn't change new ExpectedLink( - createURI('/repos/file.md'), + URI.file('/repos/file.md'), new Range(1, 4, 1, 4 + 16), new Range(1, 10, 1, 10 + 10), ), // link2 changed in the file name only new ExpectedLink( - createURI('/Users/root/tmp/file3.txt'), + URI.file('/Users/root/tmp/file3.txt'), new Range(4, 3, 4, 3 + 41), new Range(4, 18, 4, 18 + 25), ), @@ -547,11 +545,6 @@ suite('PromptsService', () => { suite('• getCombinedToolsMetadata', () => { suite('• agent mode', () => { test('• explicit', async function () { - // temporary disable the tests on for electron/nodejs on windows - if (isWindows && (isNative || isElectron)) { - this.skip(); - } - const rootFolderName = 'gets-combined-tools-metadata'; const rootFolder = `/${rootFolderName}`; @@ -681,11 +674,6 @@ suite('PromptsService', () => { }); test('• implicit', async function () { - // temporary disable the tests on for electron/nodejs on windows - if (isWindows && (isNative || isElectron)) { - this.skip(); - } - const rootFolderName = 'gets-combined-tools-metadata'; const rootFolder = `/${rootFolderName}`; @@ -814,11 +802,6 @@ suite('PromptsService', () => { }); test('• implicit (incorrect value)', async function () { - // temporary disable the tests on for electron/nodejs on windows - if (isWindows && (isNative || isElectron)) { - this.skip(); - } - const rootFolderName = 'gets-combined-tools-metadata'; const rootFolder = `/${rootFolderName}`; @@ -1074,11 +1057,6 @@ suite('PromptsService', () => { }); test('• implicit', async function () { - // temporary disable the tests on for electron/nodejs on windows - if (isWindows && (isNative || isElectron)) { - this.skip(); - } - const rootFolderName = 'gets-combined-tools-metadata'; const rootFolder = `/${rootFolderName}`; @@ -1202,11 +1180,6 @@ suite('PromptsService', () => { }); test('• implicit (incorrect value)', async function () { - // temporary disable the tests on for electron/nodejs on windows - if (isWindows && (isNative || isElectron)) { - this.skip(); - } - const rootFolderName = 'gets-combined-tools-metadata'; const rootFolder = `/${rootFolderName}`; @@ -1452,11 +1425,6 @@ suite('PromptsService', () => { }); test('• implicit', async function () { - // temporary disable the tests on for electron/nodejs on windows - if (isWindows && (isNative || isElectron)) { - this.skip(); - } - const rootFolderName = 'gets-combined-tools-metadata'; const rootFolder = `/${rootFolderName}`; @@ -1579,11 +1547,6 @@ suite('PromptsService', () => { }); test('• implicit (incorrect value)', async function () { - // temporary disable the tests on for electron/nodejs on windows - if (isWindows && (isNative || isElectron)) { - this.skip(); - } - const rootFolderName = 'gets-combined-tools-metadata'; const rootFolder = `/${rootFolderName}`; @@ -1708,11 +1671,6 @@ suite('PromptsService', () => { suite('• getAllMetadata', () => { test('• explicit', async function () { - // temporary disable the tests on for electron/nodejs on windows - if (isWindows && (isNative || isElectron)) { - this.skip(); - } - const rootFolderName = 'resolves-nested-file-references'; const rootFolder = `/${rootFolderName}`; @@ -2054,5 +2012,186 @@ suite('PromptsService', () => { 'Must find correct instruction files.', ); }); + + test('• does not have duplicates', async () => { + const rootFolderName = 'finds-instruction-files-without-duplicates'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + const userPromptsFolderName = '/tmp/user-data/prompts'; + const userPromptsFolderUri = URI.file(userPromptsFolderName); + + sinon.stub(service, 'listPromptFiles') + .returns(Promise.resolve([ + // local instructions + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md'), + storage: 'local', + type: 'instructions', + }, + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file2.instructions.md'), + storage: 'local', + type: 'instructions', + }, + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file3.instructions.md'), + storage: 'local', + type: 'instructions', + }, + { + uri: URI.joinPath(rootFolderUri, '.github/prompts/file4.instructions.md'), + storage: 'local', + type: 'instructions', + }, + // user instructions + { + uri: URI.joinPath(userPromptsFolderUri, 'file10.instructions.md'), + storage: 'user', + type: 'instructions', + }, + { + uri: URI.joinPath(userPromptsFolderUri, 'file11.instructions.md'), + storage: 'user', + type: 'instructions', + }, + ])); + + // mock current workspace file structure + await (instaService.createInstance(MockFilesystem, + [{ + name: rootFolderName, + children: [ + { + name: 'file1.prompt.md', + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + name: '.github/prompts', + children: [ + { + name: 'file1.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 1.\'', + 'applyTo: "**/*.tsx"', + '---', + 'Some instructions 1 contents.', + ], + }, + { + name: 'file2.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 2.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 2 contents.', + ], + }, + { + name: 'file3.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 3.\'', + 'applyTo: "**/folder2/*.tsx"', + '---', + 'Some instructions 3 contents.', + ], + }, + { + name: 'file4.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 4.\'', + 'applyTo: "src/build/*.tsx"', + '---', + 'Some instructions 4 contents.', + ], + }, + { + name: 'file5.prompt.md', + contents: [ + '---', + 'description: \'Prompt file 5.\'', + '---', + 'Some prompt 5 contents.', + ], + }, + ], + }, + { + name: 'folder1', + children: [ + { + name: 'main.tsx', + contents: 'console.log("Haalou!")', + }, + ], + }, + ], + }])).mock(); + + // mock user data instructions + await (instaService.createInstance(MockFilesystem, [ + { + name: userPromptsFolderName, + children: [ + { + name: 'file10.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 10.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 10 contents.', + ], + }, + { + name: 'file11.instructions.md', + contents: [ + '---', + 'description: \'Instructions file 11.\'', + 'applyTo: "**/folder1/*.py"', + '---', + 'Some instructions 11 contents.', + ], + }, + { + name: 'file12.prompt.md', + contents: [ + '---', + 'description: \'Prompt file 12.\'', + '---', + 'Some prompt 12 contents.', + ], + }, + ], + } + ])).mock(); + + const instructions = await service + .findInstructionFilesFor([ + URI.joinPath(rootFolderUri, 'folder1/main.tsx'), + URI.joinPath(rootFolderUri, 'folder1/index.tsx'), + URI.joinPath(rootFolderUri, 'folder1/constants.tsx'), + ]); + + assert.deepStrictEqual( + instructions, + [ + // local instructions + URI.joinPath(rootFolderUri, '.github/prompts/file1.instructions.md'), + URI.joinPath(rootFolderUri, '.github/prompts/file2.instructions.md'), + // user instructions + URI.joinPath(userPromptsFolderUri, 'file10.instructions.md'), + ], + 'Must find correct instruction files.', + ); + }); }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts index ea07bd5d33f..2046a69975b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/createUri.ts @@ -11,7 +11,9 @@ import { isWindows } from '../../../../../../../base/common/platform.js'; * On `Windows`, absolute paths are prefixed with the disk name. */ export const createURI = (linkPath: string): URI => { - return URI.file(createPath(linkPath)); + return URI.file( + createPath(linkPath), + ); }; /** diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index adc061690e0..39859b4a915 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -522,6 +522,7 @@ type IExtensionActionOptions = IAction2Options & { class ExtensionsContributions extends Disposable implements IWorkbenchContribution { constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService, @@ -1503,7 +1504,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi const extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; if (extension) { - const action = instantiationService.createInstance(InstallAction, { installPreReleaseVersion: this.extensionsWorkbenchService.preferPreReleases }); + const action = instantiationService.createInstance(InstallAction, { installPreReleaseVersion: this.extensionManagementService.preferPreReleases }); action.extension = extension; return action.run(); } @@ -1525,7 +1526,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi || (await this.extensionsWorkbenchService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; if (extension) { const action = instantiationService.createInstance(InstallAction, { - installPreReleaseVersion: this.extensionsWorkbenchService.preferPreReleases, + installPreReleaseVersion: this.extensionManagementService.preferPreReleases, isMachineScoped: true, }); action.extension = extension; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index a9002dc941e..862d00b9fd8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -694,12 +694,12 @@ export class InstallDropdownAction extends ButtonWithDropDownExtensionAction { constructor( @IInstantiationService instantiationService: IInstantiationService, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IWorkbenchExtensionManagementService extensionManagementService: IWorkbenchExtensionManagementService, ) { super(`extensions.installActions`, InstallAction.CLASS, [ [ - instantiationService.createInstance(InstallAction, { installPreReleaseVersion: extensionsWorkbenchService.preferPreReleases }), - instantiationService.createInstance(InstallAction, { installPreReleaseVersion: !extensionsWorkbenchService.preferPreReleases }), + instantiationService.createInstance(InstallAction, { installPreReleaseVersion: extensionManagementService.preferPreReleases }), + instantiationService.createInstance(InstallAction, { installPreReleaseVersion: !extensionManagementService.preferPreReleases }), ] ]); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ec2972994f0..8ee497e156a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -937,8 +937,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private readonly _onReset = new Emitter(); get onReset() { return this._onReset.event; } - readonly preferPreReleases: boolean; - private installing: IExtension[] = []; private tasksInProgress: CancelablePromise[] = []; @@ -982,7 +980,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, ) { super(); - this.preferPreReleases = productService.quality !== 'stable'; this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { @@ -1315,7 +1312,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const options: IQueryOptions = CancellationToken.isCancellationToken(arg1) ? {} : arg1; const token: CancellationToken = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2; options.text = options.text ? this.resolveQueryText(options.text) : options.text; - options.includePreRelease = isUndefined(options.includePreRelease) ? this.preferPreReleases : options.includePreRelease; + options.includePreRelease = isUndefined(options.includePreRelease) ? this.extensionManagementService.preferPreReleases : options.includePreRelease; const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest(); const pager = await this.galleryService.query(options, token); @@ -1339,7 +1336,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return []; } - extensionInfos.forEach(e => e.preRelease = e.preRelease ?? this.preferPreReleases); + extensionInfos.forEach(e => e.preRelease = e.preRelease ?? this.extensionManagementService.preferPreReleases); const extensionsControlManifest = await this.extensionManagementService.getExtensionsControlManifest(); const galleryExtensions = await this.galleryService.getExtensions(extensionInfos, arg1, arg2); this.syncInstalledExtensionsWithGallery(galleryExtensions); @@ -1831,7 +1828,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension async checkForUpdates(reason?: string, onlyBuiltin?: boolean): Promise { if (reason) { - this.logService.info(`[Extensions]: Checking for updates. Reason: ${reason}`); + this.logService.trace(`[Extensions]: Checking for updates. Reason: ${reason}`); } else { this.logService.trace(`[Extensions]: Checking for updates`); } @@ -2025,27 +2022,34 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private async autoUpdateExtensions(): Promise { const toUpdate: IExtension[] = []; + const disabledAutoUpdate = []; + const consentRequired = []; for (const extension of this.outdated) { if (!this.shouldAutoUpdateExtension(extension)) { - this.logService.info('Auto update disabled for extension', extension.identifier.id); + disabledAutoUpdate.push(extension.identifier.id); continue; } if (await this.shouldRequireConsentToUpdate(extension)) { - this.logService.info('Auto update consent required for extension', extension.identifier.id); + consentRequired.push(extension.identifier.id); continue; } toUpdate.push(extension); } + if (disabledAutoUpdate.length) { + this.logService.trace('Auto update disabled for extensions', disabledAutoUpdate.join(', ')); + } + + if (consentRequired.length) { + this.logService.info('Auto update consent required for extensions', consentRequired.join(', ')); + } + if (!toUpdate.length) { return; } const productVersion = this.getProductVersion(); - await Promises.settled(toUpdate.map(e => { - this.logService.info('Auto updating extension', e.identifier.id); - return this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true, productVersion } : { productVersion }); - })); + await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true, productVersion } : { productVersion }))); } private getProductVersion(): IProductVersion { @@ -2348,7 +2352,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (isString(arg)) { extension = this.local.find(e => areSameExtensions(e.identifier, { id: arg })); if (!extension?.isBuiltin) { - installableInfo = { id: arg, version: installOptions.version, preRelease: installOptions.installPreReleaseVersion ?? this.preferPreReleases }; + installableInfo = { id: arg, version: installOptions.version, preRelease: installOptions.installPreReleaseVersion ?? this.extensionManagementService.preferPreReleases }; } } // Install by gallery diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index b86d948aa1a..ff3b6412d43 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -129,7 +129,6 @@ export interface IExtensionsWorkbenchService { readonly _serviceBrand: undefined; readonly onChange: Event; readonly onReset: Event; - readonly preferPreReleases: boolean; readonly local: IExtension[]; readonly installed: IExtension[]; readonly outdated: IExtension[]; diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index 9668c594e98..fc557068ed8 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -143,7 +143,7 @@ export class SearchExtensionsTool implements IToolImpl { }], toolResultDetails: { input: JSON.stringify(params), - output: JSON.stringify(result.map(extension => extension.id)) + output: [{ type: 'text', value: JSON.stringify(result.map(extension => extension.id)) }] } }; } diff --git a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts index ab3d1dabf53..2bed6276108 100644 --- a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts +++ b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts @@ -174,7 +174,7 @@ registerAction2(class StartReadHints extends EditorAction2 { constructor() { super({ id: 'inlayHints.startReadingLineWithHint', - title: localize2('read.title', "Read Line with Inline Hints"), + title: localize2('read.title', "Read Line with Inlay Hints"), precondition: EditorContextKeys.hasInlayHintsProvider, f1: true }); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 9c387879fd8..8c3052f63c9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -55,7 +55,7 @@ import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; -import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/chatEditingService.js'; +import { ModifiedFileEntryState } from '../../chat/common/chatEditingService.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { IFileService } from '../../../../platform/files/common/files.js'; @@ -1499,16 +1499,15 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito } const chatService = accessor.get(IChatService); - const chatEditingService = accessor.get(IChatEditingService); - const uri = editor.getModel().uri; const chatModel = chatService.startSession(ChatAgentLocation.Editor, token, false); - const editSession = await chatEditingService.createEditingSession(chatModel); + chatModel.startEditingSession(true); + + const editSession = await chatModel.editingSessionObs?.promise; const store = new DisposableStore(); store.add(chatModel); - store.add(editSession); // STREAM const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); @@ -1526,11 +1525,11 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); if (!token.isCancellationRequested) { - chatRequest.response.complete(); + chatModel.completeResponse(chatRequest); } const isSettled = derived(r => { - const entry = editSession.readEntry(uri, r); + const entry = editSession?.readEntry(uri, r); if (!entry) { return false; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index 52169f74762..2ebe866eed1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -93,7 +93,11 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IConfigurationService private readonly _configurationService: IConfigurationService, ) { - this.hideOnRequest = observableConfigValue(InlineChatConfigKeys.HideOnRequest, false, this._configurationService); + + const v2 = observableConfigValue(InlineChatConfigKeys.EnableV2, false, this._configurationService); + + this.hideOnRequest = observableConfigValue(InlineChatConfigKeys.HideOnRequest, false, this._configurationService) + .map((value, r) => v2.read(r) && value); } dispose() { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index a3436539e99..99240d7a38d 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -66,7 +66,7 @@ Registry.as(Extensions.Configuration).registerConfigurat tags: ['preview', 'onExp'], }, [InlineChatConfigKeys.HideOnRequest]: { - description: localize('hideOnRequest', "Whether to hide the inline chat widget after making a request. When enabled, the widget hides after a request has been made and instead the chat overlay shows. When hidden, the widget can always be shown again with the inline chat keybinding or from the chat overlay widget."), + markdownDescription: localize('hideOnRequest', "Whether to hide the inline chat widget after making a request. When enabled, the widget hides after a request has been made and instead the chat overlay shows. When hidden, the widget can always be shown again with the inline chat keybinding or from the chat overlay widget. *Note* that this setting requires `#inlineChat.enableV2#` to be enabled."), default: false, type: 'boolean', tags: ['preview', 'onExp'], diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index b544b9602b2..4ea7121fc91 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; -import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolProgress } from '../../chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, ToolProgress } from '../../chat/common/languageModelToolsService.js'; import { McpCommandIds } from './mcpCommandIds.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServer, McpServerMetadataCache } from './mcpServer.js'; @@ -263,18 +263,22 @@ class McpToolImplementation implements IToolImpl { content: [] }; - const outputParts: string[] = []; - const callResult = await this._tool.callWithProgress(invocation.parameters as Record, progress, token); + const details: IToolResultInputOutputDetails = { + input: JSON.stringify(invocation.parameters, undefined, 2), + output: [], + isError: callResult.isError === true, + }; + for (const item of callResult.content) { if (item.type === 'text') { + details.output.push({ type: 'text', value: item.text }); result.content.push({ kind: 'text', value: item.text }); - - outputParts.push(item.text); } else if (item.type === 'image' || item.type === 'audio') { + details.output.push({ type: 'data', mimeType: item.mimeType, value64: item.data }); result.content.push({ kind: 'data', value: { mimeType: item.mimeType, data: decodeBase64(item.data) } @@ -284,11 +288,7 @@ class McpToolImplementation implements IToolImpl { } } - result.toolResultDetails = { - input: JSON.stringify(invocation.parameters, undefined, 2), - output: outputParts.join('\n') - }; - + result.toolResultDetails = details; return result; } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts index 2f1bfb218dd..4a10caf524f 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts @@ -25,6 +25,7 @@ import { IFilesConfigurationService } from '../../../services/filesConfiguration import { ILanguageSupport, ITextFileSaveOptions, ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; import { MergeEditorType } from './view/viewModel.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; export class MergeEditorInputData { constructor( @@ -69,6 +70,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService, + @ILogService private readonly logService: ILogService, ) { super(result, undefined, editorService, textFileService, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService); } @@ -188,6 +190,7 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements public updateFocusedEditor(editor: MergeEditorType): void { if (this._focusedEditor !== editor) { this._focusedEditor = editor; + this.logService.trace('alertFocusedEditor', editor); alertFocusedEditor(editor); } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 7f266b59151..64a78fe594a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -292,7 +292,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { private readonly _innerFindDomNode: HTMLElement; private readonly _focusTracker: dom.IFocusTracker; private readonly _findInputFocusTracker: dom.IFocusTracker; - private readonly _updateHistoryDelayer: Delayer; + private readonly _updateFindHistoryDelayer: Delayer; protected readonly _matchesCount!: HTMLElement; private readonly prevBtn: SimpleButton; private readonly nextBtn: SimpleButton; @@ -301,6 +301,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { private readonly _innerReplaceDomNode!: HTMLElement; private _toggleReplaceBtn!: SimpleButton; private readonly _replaceInputFocusTracker!: dom.IFocusTracker; + private readonly _updateReplaceHistoryDelayer: Delayer; protected _replaceBtn!: SimpleButton; protected _replaceAllBtn!: SimpleButton; @@ -427,12 +428,12 @@ export abstract class SimpleFindReplaceWidget extends Widget { )); // Find History with update delayer - this._updateHistoryDelayer = new Delayer(500); + this._updateFindHistoryDelayer = new Delayer(500); this.oninput(this._findInput.domNode, (e) => { this.foundMatch = this.onInputChanged(); this.updateButtons(this.foundMatch); - this._delayedUpdateHistory(); + this._delayedUpdateFindHistory(); }); this._register(this._findInput.inputBox.onDidChange(() => { @@ -585,6 +586,13 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._register(this._replaceInputFocusTracker.onDidFocus(this.onReplaceInputFocusTrackerFocus.bind(this))); this._register(this._replaceInputFocusTracker.onDidBlur(this.onReplaceInputFocusTrackerBlur.bind(this))); + // Replace History with update delayer + this._updateReplaceHistoryDelayer = new Delayer(500); + + this.oninput(this._replaceInput.domNode, (e) => { + this._delayedUpdateReplaceHistory(); + }); + this._register(this._replaceInput.inputBox.onDidChange(() => { this._state.change({ replaceString: this._replaceInput.getValue() }, true); })); @@ -875,14 +883,22 @@ export abstract class SimpleFindReplaceWidget extends Widget { } } - protected _delayedUpdateHistory() { - this._updateHistoryDelayer.trigger(this._updateHistory.bind(this)); + protected _delayedUpdateFindHistory() { + this._updateFindHistoryDelayer.trigger(this._updateFindHistory.bind(this)); } - protected _updateHistory() { + protected _updateFindHistory() { this._findInput.inputBox.addToHistory(); } + protected _delayedUpdateReplaceHistory() { + this._updateReplaceHistoryDelayer.trigger(this._updateReplaceHistory.bind(this)); + } + + protected _updateReplaceHistory() { + this._replaceInput.inputBox.addToHistory(); + } + protected _getRegexValue(): boolean { return this._findInput.getRegex(); } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index f5f2925dd5c..705d7dbb192 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -136,10 +136,10 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP this.keybindingFocusContextKey = CONTEXT_KEYBINDING_FOCUS.bindTo(this.contextKeyService); this.searchHistoryDelayer = new Delayer(500); - this.recordKeysAction = new Action(KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, localize('recordKeysLabel', "Record Keys"), ThemeIcon.asClassName(keybindingsRecordKeysIcon)); + this.recordKeysAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, localize('recordKeysLabel', "Record Keys"), ThemeIcon.asClassName(keybindingsRecordKeysIcon))); this.recordKeysAction.checked = false; - this.sortByPrecedenceAction = new Action(KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, localize('sortByPrecedeneLabel', "Sort by Precedence (Highest first)"), ThemeIcon.asClassName(keybindingsSortIcon)); + this.sortByPrecedenceAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, localize('sortByPrecedeneLabel', "Sort by Precedence (Highest first)"), ThemeIcon.asClassName(keybindingsSortIcon))); this.sortByPrecedenceAction.checked = false; this.overflowWidgetsDomNode = $('.keybindings-overflow-widgets-container.monaco-editor'); } @@ -355,7 +355,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP const fullTextSearchPlaceholder = localize('SearchKeybindings.FullTextSearchPlaceholder', "Type to search in keybindings"); const keybindingsSearchPlaceholder = localize('SearchKeybindings.KeybindingsSearchPlaceholder', "Recording Keys. Press Escape to exit"); - const clearInputAction = new Action(KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Keybindings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults()); + const clearInputAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Keybindings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults())); const searchContainer = DOM.append(this.headerContainer, $('.search-container')); this.searchWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, searchContainer, { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 833ba3ec78e..6ed14c391b6 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -170,7 +170,7 @@ export class SettingMatches { } private _toAlphaNumeric(s: string): string { - return s.replace(/[^A-Za-z0-9]+/g, ''); + return s.replace(/[^\p{L}\p{N}]+/gu, ''); } private _doFindMatchesInSetting(searchString: string, setting: ISetting): IRange[] { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 7acbd9efb85..c40d1e4015a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -1731,13 +1731,14 @@ export class SettingsEditor2 extends EditorPane { this.logService.trace('No ranked results found'); } else { this.logService.trace(`Got ranked results ${rankedResults.join(', ')}`); - const combinedResultsKeys = new Set([ + // Make a suggestion if the setting isn't in the top five results. + const firstFewResults = new Set([ ...(localResults?.filterMatches.map(m => m.setting.key) ?? []), ...(remoteResults.filterMatches.map(m => m.setting.key)) - ]); - const unlistedResults = rankedResults.filter(r => !combinedResultsKeys.has(r)); - this.logService.trace(`Got unlisted results ${unlistedResults.join(', ')}`); - this.setSearchSuggestions(unlistedResults); + ].slice(0, 5)); + const suggestedResults = rankedResults.filter(r => !firstFewResults.has(r)); + this.logService.trace(`Filtering ranked results down to ${suggestedResults.join(', ')}`); + this.setSearchSuggestions(suggestedResults); } } } diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 3e97a0e93a3..8026fbf387d 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -30,7 +30,7 @@ interface IConfiguration extends IWindowsConfiguration { editor?: { accessibilitySupport?: 'on' | 'off' | 'auto' }; security?: { workspace?: { trust?: { enabled?: boolean } }; restrictUNCAccess?: boolean }; window: IWindowSettings; - workbench?: { enableExperiments?: boolean }; + workbench?: { enableExperiments?: boolean; settings?: { showSuggestions?: boolean } }; telemetry?: { feedback?: { enabled?: boolean } }; _extensionsGallery?: { enablePPE?: boolean }; accessibility?: { verbosity?: { debug?: boolean } }; @@ -49,6 +49,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo 'editor.accessibilitySupport', 'security.workspace.trust.enabled', 'workbench.enableExperiments', + 'workbench.settings.showSuggestions', '_extensionsGallery.enablePPE', 'security.restrictUNCAccess', 'accessibility.verbosity.debug', @@ -70,6 +71,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private readonly accessibilityVerbosityDebug = new ChangeObserver('boolean'); private readonly useFileStorage = new ChangeObserver('boolean'); private readonly telemetryFeedbackEnabled = new ChangeObserver('boolean'); + private readonly showSuggestions = new ChangeObserver('boolean'); constructor( @IHostService private readonly hostService: IHostService, @@ -161,6 +163,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Enable Feedback processChanged(this.telemetryFeedbackEnabled.handleChange(config.telemetry?.feedback?.enabled)); + // Settings editor suggestions + processChanged(this.showSuggestions.handleChange(config.workbench?.settings?.showSuggestions)); + if (askToRelaunch && changed && this.hostService.hasFocus) { this.doConfirm( isNative ? diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 79f9a4c7dfc..736769dd801 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -84,6 +84,7 @@ import { ISearchTreeMatch, isSearchTreeMatch, RenderableMatch, SearchModelLocati import { INotebookFileInstanceMatch, isIMatchInNotebook } from './notebookSearch/notebookSearchModelBase.js'; import { searchMatchComparer } from './searchCompare.js'; import { AIFolderMatchWorkspaceRootImpl } from './AISearch/aiSearchModel.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; const $ = dom.$; @@ -200,7 +201,8 @@ export class SearchView extends ViewPane { @IHoverService hoverService: IHoverService, @INotebookService private readonly notebookService: INotebookService, @ILogService private readonly logService: ILogService, - @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService + @IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -1952,9 +1954,23 @@ export class SearchView extends ViewPane { } } - private handleKeywordClick(keyword: string) { + private handleKeywordClick(keyword: string, index: number, maxKeywords: number) { this.searchWidget.searchInput?.setValue(keyword); this.triggerQueryChange({ preserveFocus: false, triggeredOnType: false, shouldKeepAIResults: false }); + type KeywordClickClassification = { + owner: 'osortega'; + comment: 'Fired when the user clicks on a keyword suggestion'; + index: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The index of the keyword clicked' }; + maxKeywords: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The total number of suggested keywords' }; + }; + type KeywordClickEvent = { + index: number; + maxKeywords: number; + }; + this.telemetryService.publicLog2('searchComplete', { + index, + maxKeywords + }); } private updateKeywordSuggestion(keywords: AISearchKeyword[]) { @@ -1977,7 +1993,7 @@ export class SearchView extends ViewPane { } const button = this.messageDisposables.add(new SearchLinkButton( keyword.keyword, - () => this.handleKeywordClick(keyword.keyword), + () => this.handleKeywordClick(keyword.keyword, index, topKeywords.length), this.hoverService )); dom.append(messageEl, button.element); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index 04a69674350..0e66b1e031e 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -962,11 +962,11 @@ class ProfileNameRenderer extends ProfilePropertyRenderer { } } )); - nameInput.onDidChange(value => { + disposables.add(nameInput.onDidChange(value => { if (profileElement && value) { profileElement.root.name = value; } - }); + })); const focusTracker = disposables.add(trackFocus(nameInput.inputElement)); disposables.add(focusTracker.onDidBlur(() => { if (profileElement && !nameInput.value) { @@ -1500,7 +1500,7 @@ class ContentsProfileRenderer extends ProfilePropertyRenderer { })); }, disposables, - elementDisposables: new DisposableStore() + elementDisposables }; } @@ -1681,7 +1681,7 @@ class ProfileWorkspacesRenderer extends ProfilePropertyRenderer { })); }, disposables, - elementDisposables: new DisposableStore() + elementDisposables }; } @@ -2113,16 +2113,23 @@ interface IActionsColumnTemplateData { readonly disposables: DisposableStore; } -class ChangeProfileAction extends Action { +class ChangeProfileAction implements IAction { + + readonly id = 'changeProfile'; + readonly label = 'Change Profile'; + readonly class = ThemeIcon.asClassName(editIcon); + readonly enabled = true; + readonly tooltip = localize('change profile', "Change Profile"); + readonly checked = false; constructor( private readonly item: WorkspaceTableElement, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, ) { - super('changeProfile', '', ThemeIcon.asClassName(editIcon)); - this.tooltip = localize('change profile', "Change Profile"); } + run(): void { } + getSwitchProfileActions(): IAction[] { return this.userDataProfilesService.profiles .filter(profile => !profile.isTransient) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index bc6c1d813a0..490816ebb38 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -10,7 +10,7 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer { box-sizing: border-box; - line-height: 22px; + line-height: 16px; position: relative; overflow: hidden; height: inherit; @@ -1166,7 +1166,7 @@ /* Center dots between navigation buttons */ .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dots-container .dots-centered { display: flex; - gap: 48px; + gap: 32px; justify-content: center; align-items: center; width: max-content; @@ -1220,9 +1220,9 @@ } .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dot { - width: 19px; + width: 12px; /* Increased from 13px */ - height: 19px; + height: 12px; /* Increased from 13px */ background-color: var(--vscode-button-secondaryBackground); border: none; @@ -1239,9 +1239,9 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .step-dot.active { background-color: var(--vscode-button-background); - width: 18px; + width: 16px; /* Increased from 14px */ - height: 18px; + height: 16px; /* Increased from 14px */ } @@ -1290,7 +1290,7 @@ /* Increased from 24px */ padding: 0 16px; /* Increased from 0 11px */ - font-size: 15px; + font-size: 16px; /* Increased from default */ } @@ -1351,7 +1351,7 @@ display: flex; padding: 0 10%; align-items: center; - font-size: 24px; + font-size: 16px; } .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .multi-step-container { @@ -1375,7 +1375,8 @@ } .monaco-workbench .part.editor > .content .gettingStartedContainer.newSlide .sub-step-title { - font-size: 24px; + font-size: 20px; + line-height: 24px;; margin: 0; padding: 16px 0 4px 16px; } @@ -1401,5 +1402,5 @@ padding: 0 10%; align-items: center; min-width: max-content; - font-size: 24px; + font-size: 16px; } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg index fda9a0c3f7a..2f3dc2eceb4 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/multi-file-edits.svg @@ -1,202 +1,377 @@ - - - - - + + + + + + + - - - - - + + + + + + + + + - - - - - + + + + + + + - - - + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - - + + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - + - \ No newline at end of file + diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 37dfdb643fc..edfe3a2c557 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -49,6 +49,7 @@ import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlCon import { verifiedPublisherIcon } from './extensionsIcons.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; +import { CommontExtensionManagementService } from '../../../../platform/extensionManagement/common/abstractExtensionManagementService.js'; const TrustedPublishersStorageKey = 'extensions.trustedPublishers'; @@ -56,7 +57,7 @@ function isGalleryExtension(extension: IResourceExtension | IGalleryExtension): return extension.type === 'gallery'; } -export class ExtensionManagementService extends Disposable implements IWorkbenchExtensionManagementService { +export class ExtensionManagementService extends CommontExtensionManagementService implements IWorkbenchExtensionManagementService { declare readonly _serviceBrand: undefined; @@ -98,7 +99,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IConfigurationService protected readonly configurationService: IConfigurationService, - @IProductService protected readonly productService: IProductService, + @IProductService productService: IProductService, @IDownloadService protected readonly downloadService: IDownloadService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @IDialogService private readonly dialogService: IDialogService, @@ -108,11 +109,11 @@ export class ExtensionManagementService extends Disposable implements IWorkbench @ILogService private readonly logService: ILogService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, - @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IStorageService private readonly storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, ) { - super(); + super(productService, allowedExtensionsService); this.defaultTrustedPublishers = productService.trustedExtensionPublishers ?? []; this.workspaceExtensionManagementService = this._register(this.instantiationService.createInstance(WorkspaceExtensionsManagementService)); @@ -382,7 +383,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return Promise.reject('No Servers'); } - async canInstall(extension: IGalleryExtension | IResourceExtension): Promise { + override async canInstall(extension: IGalleryExtension | IResourceExtension): Promise { if (isGalleryExtension(extension)) { return this.canInstallGalleryExtension(extension); } diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index 10301ba95c3..1ac7ff24493 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -207,9 +207,15 @@ class WorkbenchHostService extends Disposable implements IHostService { const disposable = token.onCancellationRequested(() => { ipcRenderer.send(onCancelChannel, cancelSelectionId); }); - const elementData = this.nativeHostService.getElementData(offsetX, offsetY, token, cancelSelectionId); - elementData.finally(() => disposable.dispose()); - return elementData; + try { + const elementData = await this.nativeHostService.getElementData(offsetX, offsetY, token, cancelSelectionId); + return elementData; + } catch (error) { + disposable.dispose(); + throw new Error(`Native Host: Error getting element data: ${error}`); + } finally { + disposable.dispose(); + } } //#endregion diff --git a/src/vs/workbench/services/userDataSync/common/userDataSync.ts b/src/vs/workbench/services/userDataSync/common/userDataSync.ts index cca96a1da14..2739b6c195f 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSync.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSync.ts @@ -58,7 +58,7 @@ export function getSyncAreaLabel(source: SyncResource): string { case SyncResource.Settings: return localize('settings', "Settings"); case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); case SyncResource.Snippets: return localize('snippets', "Snippets"); - case SyncResource.Prompts: return localize('prompts', "Prompts"); + case SyncResource.Prompts: return localize('prompts', "Prompts and Instructions"); case SyncResource.Tasks: return localize('tasks', "Tasks"); case SyncResource.Extensions: return localize('extensions', "Extensions"); case SyncResource.GlobalState: return localize('ui state label', "UI State"); diff --git a/src/vs/workbench/services/views/browser/viewsService.ts b/src/vs/workbench/services/views/browser/viewsService.ts index 8fbe567e1c3..9e4053c2535 100644 --- a/src/vs/workbench/services/views/browser/viewsService.ts +++ b/src/vs/workbench/services/views/browser/viewsService.ts @@ -491,7 +491,25 @@ export class ViewsService extends Disposable implements IViewsService { category: Categories.View, precondition: ContextKeyExpr.has(`${viewDescriptor.id}.active`), keybinding: viewDescriptor.openCommandActionDescriptor?.keybindings ? { ...viewDescriptor.openCommandActionDescriptor.keybindings, weight: KeybindingWeight.WorkbenchContrib } : undefined, - f1: viewDescriptor.openCommandActionDescriptor ? true : undefined + f1: viewDescriptor.openCommandActionDescriptor ? true : undefined, + metadata: { + description: localize('open view', "Opens view {0}", viewDescriptor.name.value), + args: [ + { + name: 'options', + schema: { + type: 'object', + properties: { + 'preserveFocus': { + type: 'boolean', + default: false, + description: localize('preserveFocus', "Whether to preserve the existing focus when opening the view.") + } + }, + } + } + ] + } }); } public async run(serviceAccessor: ServicesAccessor, options?: { preserveFocus?: boolean }): Promise { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index ee93a668ed5..f1cd8e11935 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2207,6 +2207,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens onProfileAwareDidUpdateExtensionMetadata = Event.None; onDidChangeProfile = Event.None; onDidEnableExtensions = Event.None; + preferPreReleases = true; installVSIX(location: URI, manifest: Readonly, installOptions?: InstallOptions | undefined): Promise { throw new Error('Method not implemented.'); }