Merge remote-tracking branch 'origin/main' into tyriar/241128__better_parse

This commit is contained in:
Daniel Imms
2025-02-20 06:05:11 -08:00
115 changed files with 2368 additions and 3355 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
{
"kind": 2,
"language": "github-issues",
"value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"January 2025\""
"value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"February 2025\""
},
{
"kind": 1,
+1 -1
View File
@@ -7,7 +7,7 @@
{
"kind": 2,
"language": "github-issues",
"value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"January 2025\"\n\n$MINE=assignee:@me"
"value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"February 2025\"\n\n$MINE=assignee:@me"
},
{
"kind": 1,
+1 -1
View File
@@ -7,7 +7,7 @@
{
"kind": 2,
"language": "github-issues",
"value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"January 2025\"\n"
"value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"February 2025\"\n"
},
{
"kind": 1,
-10
View File
@@ -14,7 +14,6 @@
"@vscode/iconv-lite-umd": "0.7.0",
"byline": "^5.0.0",
"file-type": "16.5.4",
"jschardet": "3.1.4",
"picomatch": "2.3.1",
"vscode-uri": "^2.0.0",
"which": "4.0.0"
@@ -279,15 +278,6 @@
"node": ">=16"
}
},
"node_modules/jschardet": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz",
"integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==",
"license": "LGPL-2.1+",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/peek-readable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
+1 -1
View File
@@ -35,6 +35,7 @@
"statusBarItemTooltip",
"tabInputMultiDiff",
"tabInputTextMerge",
"textDocumentEncoding",
"textEditorDiffInformation",
"timeline"
],
@@ -3566,7 +3567,6 @@
"@vscode/iconv-lite-umd": "0.7.0",
"byline": "^5.0.0",
"file-type": "16.5.4",
"jschardet": "3.1.4",
"picomatch": "2.3.1",
"vscode-uri": "^2.0.0",
"which": "4.0.0"
+15 -4
View File
@@ -1628,19 +1628,29 @@ export class CommandCenter {
}
let modifiedUri = changes.modifiedUri;
let modifiedDocument: TextDocument | undefined;
if (!modifiedUri) {
const textEditor = window.activeTextEditor;
if (!textEditor) {
return;
}
const modifiedDocument = textEditor.document;
modifiedDocument = textEditor.document;
modifiedUri = modifiedDocument.uri;
}
if (modifiedUri.scheme !== 'file') {
return;
}
if (!modifiedDocument) {
modifiedDocument = await workspace.openTextDocument(modifiedUri);
}
const result = changes.originalWithModifiedChanges;
await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
await this.runByRepository(modifiedUri, async (repository, resource) =>
await repository.stage(resource, result, modifiedDocument.encoding));
}
@command('git.stageSelectedRanges', { diff: true })
@@ -1817,7 +1827,8 @@ export class CommandCenter {
const originalDocument = await workspace.openTextDocument(originalUri);
const result = applyLineChanges(originalDocument, modifiedDocument, changes);
await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
await this.runByRepository(modifiedUri, async (repository, resource) =>
await repository.stage(resource, result, modifiedDocument.encoding));
}
@command('git.revertChange')
@@ -1989,7 +2000,7 @@ export class CommandCenter {
this.logger.trace(`[CommandCenter][unstageSelectedRanges] invertedDiffs: ${JSON.stringify(invertedDiffs)}`);
const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
await repository.stage(modifiedUri, result);
await repository.stage(modifiedUri, result, modifiedDocument.encoding);
}
@command('git.unstageFile')
-100
View File
@@ -1,100 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as jschardet from 'jschardet';
function detectEncodingByBOM(buffer: Buffer): string | null {
if (!buffer || buffer.length < 2) {
return null;
}
const b0 = buffer.readUInt8(0);
const b1 = buffer.readUInt8(1);
// UTF-16 BE
if (b0 === 0xFE && b1 === 0xFF) {
return 'utf16be';
}
// UTF-16 LE
if (b0 === 0xFF && b1 === 0xFE) {
return 'utf16le';
}
if (buffer.length < 3) {
return null;
}
const b2 = buffer.readUInt8(2);
// UTF-8
if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) {
return 'utf8';
}
return null;
}
const IGNORE_ENCODINGS = [
'ascii',
'utf-8',
'utf-16',
'utf-32'
];
const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = {
'ibm866': 'cp866',
'big5': 'cp950'
};
const MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET: { [key: string]: string } = {
utf8: 'UTF-8',
utf16le: 'UTF-16LE',
utf16be: 'UTF-16BE',
windows1252: 'windows-1252',
windows1250: 'windows-1250',
iso88592: 'ISO-8859-2',
windows1251: 'windows-1251',
cp866: 'IBM866',
iso88595: 'ISO-8859-5',
koi8r: 'KOI8-R',
windows1253: 'windows-1253',
iso88597: 'ISO-8859-7',
windows1255: 'windows-1255',
iso88598: 'ISO-8859-8',
cp950: 'Big5',
shiftjis: 'SHIFT_JIS',
eucjp: 'EUC-JP',
euckr: 'EUC-KR',
gb2312: 'GB2312'
};
export function detectEncoding(buffer: Buffer, candidateGuessEncodings: string[]): string | null {
const result = detectEncodingByBOM(buffer);
if (result) {
return result;
}
candidateGuessEncodings = candidateGuessEncodings.map(e => MAP_CANDIDATE_GUESS_ENCODING_TO_JSCHARDET[e]).filter(e => !!e);
const detected = jschardet.detect(buffer, candidateGuessEncodings.length > 0 ? { detectEncodings: candidateGuessEncodings } : undefined);
if (!detected || !detected.encoding) {
return null;
}
const encoding = detected.encoding;
// Ignore encodings that cannot guess correctly
// (http://chardet.readthedocs.io/en/latest/supported-encodings.html)
if (0 <= IGNORE_ENCODINGS.indexOf(encoding.toLowerCase())) {
return null;
}
const normalizedEncodingName = encoding.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName];
return mapped || normalizedEncodingName;
}
-13
View File
@@ -14,7 +14,6 @@ import * as iconv from '@vscode/iconv-lite-umd';
import * as filetype from 'file-type';
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant } from './util';
import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode';
import { detectEncoding } from './encoding';
import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions } from './api/git';
import * as byline from 'byline';
import { StringDecoder } from 'string_decoder';
@@ -1330,18 +1329,6 @@ export class Repository {
.filter(entry => !!entry);
}
async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false, candidateGuessEncodings: string[] = []): Promise<string> {
const stdout = await this.buffer(object);
if (autoGuessEncoding) {
encoding = detectEncoding(stdout, candidateGuessEncodings) || encoding;
}
encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
return iconv.decode(stdout, encoding);
}
async buffer(object: string): Promise<Buffer> {
const child = this.stream(['show', '--textconv', object]);
+6 -20
View File
@@ -7,7 +7,6 @@ import TelemetryReporter from '@vscode/extension-telemetry';
import * as fs from 'fs';
import * as path from 'path';
import picomatch from 'picomatch';
import * as iconv from '@vscode/iconv-lite-umd';
import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, Uri, window, workspace, WorkspaceEdit } from 'vscode';
import { ActionButton } from './actionButton';
import { ApiRepository } from './api/api1';
@@ -25,7 +24,6 @@ import { StatusBarCommands } from './statusbar';
import { toGitUri } from './uri';
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, isRemote, Limiter, onceEvent, pathEquals, relativePath } from './util';
import { IFileWatcher, watch } from './watch';
import { detectEncoding } from './encoding';
import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider';
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
@@ -1222,19 +1220,9 @@ export class Repository implements Disposable {
await this.run(Operation.Remove, () => this.repository.rm(resources.map(r => r.fsPath)));
}
async stage(resource: Uri, contents: string): Promise<void> {
const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/');
async stage(resource: Uri, contents: string, encoding: string): Promise<void> {
await this.run(Operation.Stage, async () => {
const configFiles = workspace.getConfiguration('files', Uri.file(resource.fsPath));
let encoding = configFiles.get<string>('encoding') ?? 'utf8';
const autoGuessEncoding = configFiles.get<boolean>('autoGuessEncoding') === true;
const candidateGuessEncodings = configFiles.get<string[]>('candidateGuessEncodings') ?? [];
if (autoGuessEncoding) {
encoding = detectEncoding(Buffer.from(contents), candidateGuessEncodings) ?? encoding;
}
encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/');
await this.repository.stage(path, contents, encoding);
this._onDidChangeOriginalResource.fire(resource);
@@ -1980,17 +1968,15 @@ export class Repository implements Disposable {
async show(ref: string, filePath: string): Promise<string> {
return await this.run(Operation.Show, async () => {
const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/');
const configFiles = workspace.getConfiguration('files', Uri.file(filePath));
const defaultEncoding = configFiles.get<string>('encoding');
const autoGuessEncoding = configFiles.get<boolean>('autoGuessEncoding');
const candidateGuessEncodings = configFiles.get<string[]>('candidateGuessEncodings');
try {
return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings);
const content = await this.repository.buffer(`${ref}:${path}`);
return await workspace.decode(content, Uri.file(filePath));
} catch (err) {
if (err.gitErrorCode === GitErrorCodes.WrongCase) {
const gitRelativePath = await this.repository.getGitRelativePath(ref, path);
return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding, candidateGuessEncodings);
const content = await this.repository.buffer(`${ref}:${gitRelativePath}`);
return await workspace.decode(content, Uri.file(filePath));
}
throw err;
+1
View File
@@ -25,6 +25,7 @@
"../../src/vscode-dts/vscode.proposed.statusBarItemTooltip.d.ts",
"../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts",
"../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts",
"../../src/vscode-dts/vscode.proposed.textDocumentEncoding.d.ts",
"../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts",
"../../src/vscode-dts/vscode.proposed.timeline.d.ts",
"../types/lib.textEncoder.d.ts"
@@ -239,7 +239,6 @@ export async function getCompletionItemsFromSpecs(
for (const command of availableCommands) {
const commandTextLabel = typeof command.label === 'string' ? command.label : command.label.label;
if (!labels.has(commandTextLabel)) {
//TODO: maybe prioritize builtin label over spec's
items.push(createCompletionItem(
terminalContext.cursorPosition,
prefix,
@@ -248,6 +247,15 @@ export async function getCompletionItemsFromSpecs(
command.documentation
));
labels.add(commandTextLabel);
} else {
const existingItem = items.find(i => (typeof i.label === 'string' ? i.label : i.label.label) === commandTextLabel);
if (!existingItem) {
continue;
}
const preferredItem = compareItems(existingItem, command);
if (preferredItem) {
items.splice(items.indexOf(existingItem), 1, preferredItem);
}
}
}
filesRequested = true;
@@ -269,6 +277,20 @@ export async function getCompletionItemsFromSpecs(
return { items, filesRequested, foldersRequested, cwd };
}
function compareItems(existingItem: vscode.TerminalCompletionItem, command: ICompletionResource): vscode.TerminalCompletionItem | undefined {
let score = typeof command.label === 'object' ? (command.label.detail !== undefined ? 1 : 0) : 0;
score += typeof command.label === 'object' ? (command.label.description !== undefined ? 2 : 0) : 0;
score += command.documentation ? typeof command.documentation === 'string' ? 2 : 3 : 0;
if (score > 0) {
score -= typeof existingItem.label === 'object' ? (existingItem.label.detail !== undefined ? 1 : 0) : 0;
score -= typeof existingItem.label === 'object' ? (existingItem.label.description !== undefined ? 2 : 0) : 0;
score -= existingItem.documentation ? typeof existingItem.documentation === 'string' ? 2 : 3 : 0;
if (score >= 0) {
return { ...command, replacementIndex: existingItem.replacementIndex, replacementLength: existingItem.replacementLength };
}
}
}
function getShell(shellType: TerminalShellType): string | undefined {
switch (shellType) {
case TerminalShellType.Bash:
@@ -1311,7 +1311,7 @@ suite('vscode API - workspace', () => {
return deleteFile(file);
}
test('text document encodings', async () => {
test('encoding: text document encodings', async () => {
const uri1 = await createRandomFile();
const uri2 = await createRandomFile(new Uint8Array([0xEF, 0xBB, 0xBF]) /* UTF-8 with BOM */);
const uri3 = await createRandomFile(new Uint8Array([0xFF, 0xFE]) /* UTF-16 LE BOM */);
@@ -1333,7 +1333,66 @@ suite('vscode API - workspace', () => {
assert.strictEqual(doc5.encoding, 'utf8');
});
test('fs.decode', async function () {
test('encoding: openTextDocument', async () => {
const uri1 = await createRandomFile();
let doc1 = await vscode.workspace.openTextDocument(uri1, { encoding: 'cp1252' });
assert.strictEqual(doc1.encoding, 'cp1252');
let listener: vscode.Disposable | undefined;
const documentChangePromise = new Promise<void>(resolve => {
listener = vscode.workspace.onDidChangeTextDocument(e => {
if (e.document.uri.toString() === uri1.toString()) {
resolve();
}
});
});
doc1 = await vscode.workspace.openTextDocument(uri1, { encoding: 'utf16le' });
assert.strictEqual(doc1.encoding, 'utf16le');
await documentChangePromise;
const doc2 = await vscode.workspace.openTextDocument({ encoding: 'utf16be' });
assert.strictEqual(doc2.encoding, 'utf16be');
const doc3 = await vscode.workspace.openTextDocument({ content: 'Hello World', encoding: 'utf16le' });
assert.strictEqual(doc3.encoding, 'utf16le');
listener?.dispose();
});
test('encoding: openTextDocument - throws for dirty documents', async () => {
const uri1 = await createRandomFile();
const doc1 = await vscode.workspace.openTextDocument(uri1, { encoding: 'cp1252' });
const edit = new vscode.WorkspaceEdit();
edit.insert(doc1.uri, new vscode.Position(0, 0), 'Hello World');
await vscode.workspace.applyEdit(edit);
assert.strictEqual(doc1.isDirty, true);
let err;
try {
await vscode.workspace.decode(new Uint8Array([0, 0, 0, 0]), doc1.uri);
} catch (e) {
err = e;
}
assert.ok(err);
});
test('encoding: openTextDocument - multiple requests with different encoding work', async () => {
const uri1 = await createRandomFile();
const doc1P = vscode.workspace.openTextDocument(uri1);
const doc2P = vscode.workspace.openTextDocument(uri1, { encoding: 'cp1252' });
const [doc1, doc2] = await Promise.all([doc1P, doc2P]);
assert.strictEqual(doc1.encoding, 'cp1252');
assert.strictEqual(doc2.encoding, 'cp1252');
});
test('encoding: decode', async function () {
const uri = root.with({ path: posix.join(root.path, 'file.txt') });
// without setting
@@ -1375,7 +1434,7 @@ suite('vscode API - workspace', () => {
assert.ok(err);
});
test('fs.encode', async function () {
test('encoding: encode', async function () {
const uri = root.with({ path: posix.join(root.path, 'file.txt') });
// without setting
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.98.0",
"distro": "b37092a45bb95e5098cefcf6809fa6973cfd2538",
"distro": "18d6e62cad16da70804e341893ae344d5cdcc6ee",
"author": {
"name": "Microsoft Corporation"
},
@@ -30,11 +30,6 @@ export class FocusTracker extends Disposable {
return;
}
this._isFocused = focused;
if (this._isFocused) {
this._domNode.focus();
} else {
this._domNode.blur();
}
this._onFocusChange(this._isFocused);
}
@@ -233,6 +233,7 @@ export class ObservableCodeEditor extends Disposable {
public readonly layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft);
public readonly layoutInfoWidth = this.layoutInfo.map(l => l.width);
public readonly layoutInfoMinimap = this.layoutInfo.map(l => l.minimap);
public readonly layoutInfoVerticalScrollbarWidth = this.layoutInfo.map(l => l.verticalScrollbarWidth);
public readonly contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth());
@@ -7,7 +7,7 @@ import * as dom from '../../../base/browser/dom.js';
import * as domStylesheets from '../../../base/browser/domStylesheets.js';
import * as cssJs from '../../../base/browser/cssValue.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { IDisposable, DisposableStore, Disposable, toDisposable } from '../../../base/common/lifecycle.js';
import { IDisposable, DisposableStore, Disposable, toDisposable, DisposableMap } from '../../../base/common/lifecycle.js';
import { LinkedList } from '../../../base/common/linkedList.js';
import * as strings from '../../../base/common/strings.js';
import { URI } from '../../../base/common/uri.js';
@@ -210,7 +210,7 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC
return provider.resolveDecorationCSSRules();
}
private readonly _transientWatchers: { [uri: string]: ModelTransientSettingWatcher } = {};
private readonly _transientWatchers = this._register(new DisposableMap<string, ModelTransientSettingWatcher>());
private readonly _modelProperties = new Map<string, Map<string, any>>();
public setModelProperty(resource: URI, key: string, value: any): void {
@@ -238,12 +238,10 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC
public setTransientModelProperty(model: ITextModel, key: string, value: any): void {
const uri = model.uri.toString();
let w: ModelTransientSettingWatcher;
if (this._transientWatchers.hasOwnProperty(uri)) {
w = this._transientWatchers[uri];
} else {
let w = this._transientWatchers.get(uri);
if (!w) {
w = new ModelTransientSettingWatcher(uri, model, this);
this._transientWatchers[uri] = w;
this._transientWatchers.set(uri, w);
}
const previousValue = w.get(key);
@@ -256,25 +254,27 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC
public getTransientModelProperty(model: ITextModel, key: string): any {
const uri = model.uri.toString();
if (!this._transientWatchers.hasOwnProperty(uri)) {
const watcher = this._transientWatchers.get(uri);
if (!watcher) {
return undefined;
}
return this._transientWatchers[uri].get(key);
return watcher.get(key);
}
public getTransientModelProperties(model: ITextModel): [string, any][] | undefined {
const uri = model.uri.toString();
if (!this._transientWatchers.hasOwnProperty(uri)) {
const watcher = this._transientWatchers.get(uri);
if (!watcher) {
return undefined;
}
return this._transientWatchers[uri].keys().map(key => [key, this._transientWatchers[uri].get(key)]);
return watcher.keys().map(key => [key, watcher.get(key)]);
}
_removeWatcher(w: ModelTransientSettingWatcher): void {
delete this._transientWatchers[w.uri];
this._transientWatchers.deleteAndDispose(w.uri);
}
abstract getActiveCodeEditor(): ICodeEditor | null;
@@ -295,14 +295,16 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC
}
}
export class ModelTransientSettingWatcher {
export class ModelTransientSettingWatcher extends Disposable {
public readonly uri: string;
private readonly _values: { [key: string]: any };
constructor(uri: string, model: ITextModel, owner: AbstractCodeEditorService) {
super();
this.uri = uri;
this._values = {};
model.onWillDispose(() => owner._removeWatcher(this));
this._register(model.onWillDispose(() => owner._removeWatcher(this)));
}
public set(key: string, value: any): void {
@@ -47,7 +47,7 @@ export class GhostText {
const text = new TextEdit([
...this.parts.map(p => new SingleTextEdit(
Range.fromPositions(new Position(1, p.column)),
p.lines.join('\n')
p.lines.map(line => line.line).join('\n')
)),
]).applyToString(cappedLineText);
@@ -70,14 +70,14 @@ export class GutterIndicatorMenuContent {
})),
option(createOptionArgs({ id: 'reject', title: localize('reject', "Reject"), icon: Codicon.close, commandId: hideInlineCompletionId })),
separator(),
this._inlineEditsShowCollapsed.map(showCollapsed => showCollapsed ?
option(createOptionArgs({ id: 'showExpanded', title: localize('showExpanded', "Show Expanded"), icon: Codicon.expandAll, commandId: toggleShowCollapsedId })) :
option(createOptionArgs({ id: 'showCollapsed', title: localize('showCollapsed', "Show Collapsed"), icon: Codicon.collapseAll, commandId: toggleShowCollapsedId }))
),
this._host.extensionCommands?.map(c => c && c.length > 0 ? [
...c.map((c, idx) => option(createOptionArgs({ id: c.id + '_' + idx, title: c.title, icon: Codicon.symbolEvent, commandId: c.id, commandArgs: c.arguments }))),
separator()
] : []),
this._inlineEditsShowCollapsed.map(showCollapsed => showCollapsed ?
option(createOptionArgs({ id: 'showExpanded', title: localize('showExpanded', "Show Expanded"), icon: Codicon.expandAll, commandId: toggleShowCollapsedId })) :
option(createOptionArgs({ id: 'showCollapsed', title: localize('showCollapsed', "Show Collapsed"), icon: Codicon.collapseAll, commandId: toggleShowCollapsedId }))
),
option(createOptionArgs({ id: 'settings', title: localize('settings', "Settings"), icon: Codicon.gear, commandId: 'workbench.action.openSettings', commandArgs: ['@tag:nextEditSuggestions'] })),
this._host.action.map(action => action ? [
separator(),
@@ -53,7 +53,12 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits
const eol = textModel.getEOL();
const startsWithEol = state.text.startsWith(eol);
const originalRange = new LineRange(state.lineNumber, state.lineNumber + (startsWithEol ? 0 : 1));
const modifiedLines = state.text.split(eol).splice(startsWithEol ? 0 : 1);
let modifiedLines = state.text.split(eol);
if (startsWithEol) {
modifiedLines = modifiedLines.splice(1);
} else {
modifiedLines[0] = textModel.getLineContent(state.lineNumber) + modifiedLines[0];
}
return getPrefixTrim([], originalRange, modifiedLines, this._editor);
});
@@ -100,6 +100,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin
const lineHeight = this._editor.getOption(EditorOption.lineHeight).read(reader);
const contentLeft = this._editor.layoutInfoContentLeft.read(reader);
const verticalScrollbarWidth = this._editor.layoutInfoVerticalScrollbarWidth.read(reader);
const scrollLeft = this._editor.scrollLeft.read(reader);
const scrollTop = this._editor.scrollTop.read(reader);
const editorLeftOffset = contentLeft - scrollLeft;
@@ -140,7 +141,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin
lowerBackground,
lowerText,
padding: PADDING,
minContentWidthRequired: maxLineWidth + PADDING * 2,
minContentWidthRequired: prefixLeftOffset + maxLineWidth + PADDING * 2 + verticalScrollbarWidth,
};
});
@@ -81,10 +81,22 @@ export function getPrefixTrim(diffRanges: Range[], originalLinesRange: LineRange
const replacementStart = diffRanges.map(r => r.isSingleLine() ? r.startColumn - 1 : 0);
const originalIndents = originalLinesRange.mapToLineArray(line => indentOfLine(textModel.getLineContent(line)));
const modifiedIndents = modifiedLines.map(line => indentOfLine(line));
const modifiedIndents = modifiedLines.filter(line => line !== '').map(line => indentOfLine(line));
const prefixTrim = Math.min(...replacementStart, ...originalIndents, ...modifiedIndents);
const prefixLeftOffset = editor.getOffsetForColumn(originalLinesRange.startLineNumber, prefixTrim + 1);
let prefixLeftOffset;
const startLineIndent = textModel.getLineIndentColumn(originalLinesRange.startLineNumber);
if (startLineIndent >= prefixTrim + 1) {
// We can use the editor to get the offset
prefixLeftOffset = editor.getOffsetForColumn(originalLinesRange.startLineNumber, prefixTrim + 1);
} else if (startLineIndent !== 1) {
// We need to approximate the offset as the editor does not contain the modified lines yet
const startLineIndentOffset = editor.getOffsetForColumn(originalLinesRange.startLineNumber, startLineIndent);
prefixLeftOffset = startLineIndentOffset / (startLineIndent - 1) * prefixTrim;
} else {
// unable to approximate the offset
return { prefixTrim: 0, prefixLeftOffset: 0 };
}
return { prefixTrim, prefixLeftOffset };
}
@@ -14,7 +14,7 @@ import { Event } from '../../../base/common/event.js';
import { UILabelProvider } from '../../../base/common/keybindingLabels.js';
import { ResolvedKeybinding } from '../../../base/common/keybindings.js';
import { KeyCode } from '../../../base/common/keyCodes.js';
import { combinedDisposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { combinedDisposable, DisposableStore, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { isLinux, isWindows, OS } from '../../../base/common/platform.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { assertType } from '../../../base/common/types.js';
@@ -429,6 +429,7 @@ export interface IDropdownWithDefaultActionViewItemOptions extends IDropdownMenu
export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
private readonly _options: IDropdownWithDefaultActionViewItemOptions | undefined;
private _defaultAction: ActionViewItem;
private readonly _defaultActionDisposables = this._register(new DisposableStore());
private readonly _dropdown: DropdownMenuActionViewItem;
private _container: HTMLElement | null = null;
private readonly _storageKey: string;
@@ -471,7 +472,7 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
actionRunner: options?.actionRunner ?? this._register(new ActionRunner()),
};
this._dropdown = new DropdownMenuActionViewItem(submenuAction, submenuAction.actions, this._contextMenuService, dropdownOptions);
this._dropdown = this._register(new DropdownMenuActionViewItem(submenuAction, submenuAction.actions, this._contextMenuService, dropdownOptions));
this._register(this._dropdown.actionRunner.onDidRun((e: IRunEvent) => {
if (e.action instanceof MenuItemAction) {
this.update(e.action);
@@ -484,13 +485,13 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.MACHINE);
}
this._defaultAction.dispose();
this._defaultAction = this._instaService.createInstance(MenuEntryActionViewItem, lastAction, { keybinding: this._getDefaultActionKeybindingLabel(lastAction) });
this._defaultAction.actionRunner = new class extends ActionRunner {
this._defaultActionDisposables.clear();
this._defaultAction = this._defaultActionDisposables.add(this._instaService.createInstance(MenuEntryActionViewItem, lastAction, { keybinding: this._getDefaultActionKeybindingLabel(lastAction) }));
this._defaultAction.actionRunner = this._defaultActionDisposables.add(new class extends ActionRunner {
protected override async runAction(action: IAction, context?: unknown): Promise<void> {
await action.run(undefined);
}
}();
}());
if (this._container) {
this._defaultAction.render(prepend(this._container, $('.action-container')));
@@ -567,12 +568,6 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
this._dropdown.setFocusable(false);
}
}
override dispose() {
this._defaultAction.dispose();
this._dropdown.dispose();
super.dispose();
}
}
class SubmenuEntrySelectActionViewItem extends SelectActionViewItem {
+26 -6
View File
@@ -13,7 +13,7 @@ import { ResourceMap } from '../../../base/common/map.js';
import { parse } from '../../../base/common/marshalling.js';
import { Schemas } from '../../../base/common/network.js';
import { isNative, isWeb } from '../../../base/common/platform.js';
import { URI } from '../../../base/common/uri.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { localize } from '../../../nls.js';
import { IDialogService } from '../../dialogs/common/dialogs.js';
import { IBaseTextResourceEditorInput, ITextEditorSelection } from '../../editor/common/editor.js';
@@ -23,6 +23,7 @@ import { ByteSize, IFileService } from '../../files/common/files.js';
import { IInstantiationService, ServicesAccessor } from '../../instantiation/common/instantiation.js';
import { extractSelection } from '../../opener/common/opener.js';
import { Registry } from '../../registry/common/platform.js';
import { IMarker } from '../../markers/common/markers.js';
//#region Editor / Resources DND
@@ -30,7 +31,8 @@ import { Registry } from '../../registry/common/platform.js';
export const CodeDataTransfers = {
EDITORS: 'CodeEditors',
FILES: 'CodeFiles',
SYMBOLS: 'application/vnd.code.symbols'
SYMBOLS: 'application/vnd.code.symbols',
MARKERS: 'application/vnd.code.diagnostics',
};
export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput {
@@ -414,8 +416,12 @@ export interface DocumentSymbolTransferData {
kind: number;
}
export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] {
const rawSymbolsData = e.dataTransfer?.getData(CodeDataTransfers.SYMBOLS);
function setDataAsJSON(e: DragEvent, kind: string, data: unknown) {
e.dataTransfer?.setData(kind, JSON.stringify(data));
}
function getDataAsJSON<T>(e: DragEvent, kind: string, defaultValue: T): T {
const rawSymbolsData = e.dataTransfer?.getData(kind);
if (rawSymbolsData) {
try {
return JSON.parse(rawSymbolsData);
@@ -424,11 +430,25 @@ export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[
}
}
return [];
return defaultValue;
}
export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] {
return getDataAsJSON(e, CodeDataTransfers.SYMBOLS, []);
}
export function fillInSymbolsDragData(symbolsData: readonly DocumentSymbolTransferData[], e: DragEvent): void {
e.dataTransfer?.setData(CodeDataTransfers.SYMBOLS, JSON.stringify(symbolsData));
setDataAsJSON(e, CodeDataTransfers.SYMBOLS, symbolsData);
}
export type MarkerTransferData = IMarker | { uri: UriComponents };
export function extractMarkerDropData(e: DragEvent): MarkerTransferData[] | undefined {
return getDataAsJSON(e, CodeDataTransfers.MARKERS, undefined);
}
export function fillInMarkersDragData(markerData: MarkerTransferData[], e: DragEvent): void {
setDataAsJSON(e, CodeDataTransfers.MARKERS, markerData);
}
/**
@@ -44,6 +44,9 @@ const _allApiProposals = {
chatReferenceBinaryData: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceBinaryData.d.ts',
},
chatReferenceDiagnostic: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts',
},
chatTab: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts',
},
@@ -64,6 +64,15 @@ export namespace MarkerSeverity {
return _displayStrings[a] || '';
}
const _displayStringsPlural: { [value: number]: string } = Object.create(null);
_displayStringsPlural[MarkerSeverity.Error] = localize('sev.errors', "Errors");
_displayStringsPlural[MarkerSeverity.Warning] = localize('sev.warnings', "Warnings");
_displayStringsPlural[MarkerSeverity.Info] = localize('sev.infos', "Infos");
export function toStringPlural(a: MarkerSeverity): string {
return _displayStringsPlural[a] || '';
}
export function fromSeverity(severity: Severity): MarkerSeverity {
switch (severity) {
case Severity.Error: return MarkerSeverity.Error;
+185
View File
@@ -0,0 +1,185 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from '../../configuration/common/configuration.js';
/**
* Configuration helper for the `reusable prompts` feature.
* @see {@link CONFIG_KEY}.
*
* ### Functions
*
* - {@link getValue} allows to current read configuration value
* - {@link enabled} allows to check if the feature is enabled
* - {@link promptSourceFolders} gets list of source folders for prompt files
*
* ### Configuration Examples
*
* Enable the feature using the default `'.github/prompts'` folder as a source of prompt files:
* ```json
* {
* "chat.promptFiles": {},
* }
* ```
*
* Enable the feature, providing multiple source folder paths for prompt files,
* in addition to the default `'.github/prompts'` one:
* ```json
* {
* "chat.promptFiles": {
* ".copilot/prompts" : false,
* "/Users/legomushroom/repos/prompts" : true,
* },
* }
* ```
*
* See the next section for details on how we treat the config value.
*
* ### Possible Values
*
* - `undefined`/`null`: feature is disabledx
* - `object`:
* - expects the { "string": `boolean` } pairs, where the `string` is a path and the `boolean`
* is a flag that defines if this additional source folder is enabled or disabled;
* enabled source folders are used in addition to the default {@link DEFAULT_SOURCE_FOLDER} path;
* you can explicitly disable the default source folder by setting it to `false` in the object
* - value of a record in the object can also be a `string`:
* - if the string can be clearly mapped to a `boolean` (e.g., `"true"`, `"FALSE", "TrUe"`, etc.),
* it is treated as `boolean` value
* - any other string value is treated as `false` and is effectively ignored
* - if the record `key` is an `empty` string, it is ignored
* - if the resulting object is empty, the feature is considered `enabled`, prompt files source
* folders fallback to the default {@link DEFAULT_SOURCE_FOLDER} path
* - if the resulting object is not empty, and the default {@link DEFAULT_SOURCE_FOLDER} path
* is not explicitly disabled, it is added to the list of prompt files source folders
*
* ### File Paths Resolution
*
* We resolve only `*.prompt.md` files inside the resulting source folders. Relative paths are resolved
* relative to:
*
* - the current workspace `root`, if applicable, in other words one of the workspace folders
* can be used as a prompt files source folder
* - root of each top-level folder in the workspace (if there are multiple workspace folders)
* - current root folder (if a single folder is open)
*/
export namespace PromptsConfig {
/**
* Configuration key for the `prompt files` feature (also
* known as `prompt files`, `prompt instructions`, etc.).
*/
export const CONFIG_KEY: string = 'chat.promptFiles';
/**
* Default reusable prompt files source folder.
*/
export const DEFAULT_SOURCE_FOLDER = '.github/prompts';
/**
* Get value of the `prompt files` configuration setting.
*/
export const getValue = (
configService: IConfigurationService,
): Record<string, boolean> | undefined => {
const configValue = configService.getValue(CONFIG_KEY);
if (configValue === undefined || configValue === null || Array.isArray(configValue)) {
return undefined;
}
// note! this would be also true for `null` and `array`,
// but those cases are already handled above
if (typeof configValue === 'object') {
const paths: Record<string, boolean> = {};
for (const [path, value] of Object.entries(configValue)) {
const cleanPath = path.trim();
const booleanValue = asBoolean(value);
// if value can be mapped to a boolean, and the clean
// path is not empty, add it to the map
if ((booleanValue !== undefined) && cleanPath) {
paths[cleanPath] = booleanValue;
}
}
return paths;
}
return undefined;
};
/**
* Checks if the feature is enabled.
*/
export const enabled = (
configService: IConfigurationService,
): boolean => {
const value = getValue(configService);
return value !== undefined;
};
/**
* Gets list of source folders for prompt files.
* Defaults to {@link DEFAULT_SOURCE_FOLDER}.
*/
export const promptSourceFolders = (
configService: IConfigurationService,
): string[] => {
const value = getValue(configService);
// note! the `value &&` part handles the `undefined`, `null`, and `false` cases
if (value && (typeof value === 'object')) {
const paths: string[] = [];
// if the default source folder is not explicitly disabled, add it
if (value[DEFAULT_SOURCE_FOLDER] !== false) {
paths.push(DEFAULT_SOURCE_FOLDER);
}
// copy all the enabled paths to the result list
for (const [path, enabled] of Object.entries(value)) {
if (enabled && path !== DEFAULT_SOURCE_FOLDER) {
paths.push(path);
}
}
return paths;
}
// `undefined`, `null`, and `false` cases
return [];
};
}
/**
* Helper to parse an input value of `any` type into a boolean.
*
* @param value - input value to parse
* @returns `true` if the value is the boolean `true` value or a string that can
* be clearly mapped to a boolean (e.g., `"true"`, `"TRUE"`, `"FaLSe"`, etc.),
* `undefined` for rest of the values
*/
function asBoolean(value: any): boolean | undefined {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
const cleanValue = value.trim().toLowerCase();
if (cleanValue === 'true') {
return true;
}
if (cleanValue === 'false') {
return false;
}
return undefined;
}
return undefined;
}
@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from '../../../base/common/uri.js';
import { assert } from '../../../base/common/assert.js';
import { basename } from '../../../base/common/path.js';
/**
* File extension for the reusable prompt files.
*/
export const PROMPT_FILE_EXTENSION = '.prompt.md';
/**
* Check if provided path is a prompt file.
*/
export const isPromptFile = (
fileUri: URI,
): boolean => {
return fileUri
.path
.endsWith(PROMPT_FILE_EXTENSION);
};
/**
* Gets clean prompt name without file extension.
*
* @throws If provided path is not a prompt file
* (does not end with {@link PROMPT_FILE_EXTENSION}).
*/
export const getCleanPromptName = (
fileUri: URI,
): string => {
assert(
isPromptFile(fileUri),
`Provided path '${fileUri.fsPath}' is not a prompt file.`,
);
return basename(fileUri.path, PROMPT_FILE_EXTENSION);
};
@@ -0,0 +1,331 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { mockService } from './utils/mock.js';
import { PromptsConfig } from '../../common/config.js';
import { randomInt } from '../../../../base/common/numbers.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { IConfigurationOverrides, IConfigurationService } from '../../../configuration/common/configuration.js';
/**
* Mocked instance of {@link IConfigurationService}.
*/
const createMock = <T>(value: T): IConfigurationService => {
return mockService<IConfigurationService>({
getValue(key?: string | IConfigurationOverrides) {
assert.strictEqual(
key,
PromptsConfig.CONFIG_KEY,
`Mocked service supports only one configuration key: '${PromptsConfig.CONFIG_KEY}'.`,
);
return value;
},
});
};
suite('PromptsConfig', () => {
ensureNoDisposablesAreLeakedInTestSuite();
suite('• getValue', () => {
test('• undefined', () => {
const configService = createMock(undefined);
assert.strictEqual(
PromptsConfig.getValue(configService),
undefined,
'Must read correct value.',
);
});
test('• null', () => {
const configService = createMock(null);
assert.strictEqual(
PromptsConfig.getValue(configService),
undefined,
'Must read correct value.',
);
});
suite('• object', () => {
test('• empty', () => {
assert.deepStrictEqual(
PromptsConfig.getValue(createMock({})),
{},
'Must read correct value.',
);
});
test('• only valid strings', () => {
assert.deepStrictEqual(
PromptsConfig.getValue(createMock({
'/root/.bashrc': true,
'../../folder/.hidden-folder/config.xml': true,
'/srv/www/Public_html/.htaccess': true,
'../../another.folder/.WEIRD_FILE.log': true,
'./folder.name/file.name': true,
'/media/external/backup.tar.gz': true,
'/Media/external/.secret.backup': true,
'../relative/path.to.file': true,
'./folderName.with.dots/more.dots.extension': true,
'some/folder.with.dots/another.file': true,
'/var/logs/app.01.05.error': true,
'./.tempfile': true,
})),
{
'/root/.bashrc': true,
'../../folder/.hidden-folder/config.xml': true,
'/srv/www/Public_html/.htaccess': true,
'../../another.folder/.WEIRD_FILE.log': true,
'./folder.name/file.name': true,
'/media/external/backup.tar.gz': true,
'/Media/external/.secret.backup': true,
'../relative/path.to.file': true,
'./folderName.with.dots/more.dots.extension': true,
'some/folder.with.dots/another.file': true,
'/var/logs/app.01.05.error': true,
'./.tempfile': true,
},
'Must read correct value.',
);
});
test('• filters out non valid entries', () => {
assert.deepStrictEqual(
PromptsConfig.getValue(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'../assets/img/logo.v2.png': true,
'/mnt/storage/video.archive/episode.01.mkv': false,
'../.local/bin/script.sh': true,
'/usr/local/share/.fonts/CustomFont.otf': '',
'../../development/branch.name/some.test': true,
'/Home/user/.ssh/config': true,
'./hidden.dir/.subhidden': '\f',
'/tmp/.temp.folder/cache.db': true,
'/opt/software/v3.2.1/build.log': ' ',
'': true,
'./scripts/.old.build.sh': true,
'/var/data/datafile.2025-02-05.json': '\n',
'\n\n': true,
'\t': true,
'\v': true,
'\f': true,
'\r\n': true,
'\f\f': true,
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
{
'../assets/img/logo.v2.png': true,
'/mnt/storage/video.archive/episode.01.mkv': false,
'../.local/bin/script.sh': true,
'../../development/branch.name/some.test': true,
'/Home/user/.ssh/config': true,
'/tmp/.temp.folder/cache.db': true,
'./scripts/.old.build.sh': true,
},
'Must read correct value.',
);
});
test('• only invalid or false values', () => {
assert.deepStrictEqual(
PromptsConfig.getValue(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'../assets/IMG/logo.v2.png': '',
'/mnt/storage/video.archive/episode.01.mkv': false,
'/usr/local/share/.fonts/CustomFont.otf': '',
'./hidden.dir/.subhidden': '\f',
'/opt/Software/v3.2.1/build.log': ' ',
'/var/data/datafile.2025-02-05.json': '\n',
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
{
'/mnt/storage/video.archive/episode.01.mkv': false,
},
'Must read correct value.',
);
});
});
});
suite('• sourceLocations', () => {
test('• undefined', () => {
const configService = createMock(undefined);
assert.deepStrictEqual(
PromptsConfig.promptSourceFolders(configService),
[],
'Must read correct value.',
);
});
test('• null', () => {
const configService = createMock(null);
assert.deepStrictEqual(
PromptsConfig.promptSourceFolders(configService),
[],
'Must read correct value.',
);
});
suite('object', () => {
test('empty', () => {
assert.deepStrictEqual(
PromptsConfig.promptSourceFolders(createMock({})),
['.github/prompts'],
'Must read correct value.',
);
});
test('only valid strings', () => {
assert.deepStrictEqual(
PromptsConfig.promptSourceFolders(createMock({
'/root/.bashrc': true,
'../../folder/.hidden-folder/config.xml': true,
'/srv/www/Public_html/.htaccess': true,
'../../another.folder/.WEIRD_FILE.log': true,
'./folder.name/file.name': true,
'/media/external/backup.tar.gz': true,
'/Media/external/.secret.backup': true,
'../relative/path.to.file': true,
'./folderName.with.dots/more.dots.extension': true,
'some/folder.with.dots/another.file': true,
'/var/logs/app.01.05.error': true,
'.GitHub/prompts': true,
'./.tempfile': true,
})),
[
'.github/prompts',
'/root/.bashrc',
'../../folder/.hidden-folder/config.xml',
'/srv/www/Public_html/.htaccess',
'../../another.folder/.WEIRD_FILE.log',
'./folder.name/file.name',
'/media/external/backup.tar.gz',
'/Media/external/.secret.backup',
'../relative/path.to.file',
'./folderName.with.dots/more.dots.extension',
'some/folder.with.dots/another.file',
'/var/logs/app.01.05.error',
'.GitHub/prompts',
'./.tempfile',
],
'Must read correct value.',
);
});
test('filters out non valid entries', () => {
assert.deepStrictEqual(
PromptsConfig.promptSourceFolders(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'../assets/img/logo.v2.png': true,
'/mnt/storage/video.archive/episode.01.mkv': false,
'../.local/bin/script.sh': true,
'/usr/local/share/.fonts/CustomFont.otf': '',
'../../development/branch.name/some.test': true,
'.giThub/prompts': true,
'/Home/user/.ssh/config': true,
'./hidden.dir/.subhidden': '\f',
'/tmp/.temp.folder/cache.db': true,
'.github/prompts': true,
'/opt/software/v3.2.1/build.log': ' ',
'': true,
'./scripts/.old.build.sh': true,
'/var/data/datafile.2025-02-05.json': '\n',
'\n\n': true,
'\t': true,
'\v': true,
'\f': true,
'\r\n': true,
'\f\f': true,
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
[
'.github/prompts',
'../assets/img/logo.v2.png',
'../.local/bin/script.sh',
'../../development/branch.name/some.test',
'.giThub/prompts',
'/Home/user/.ssh/config',
'/tmp/.temp.folder/cache.db',
'./scripts/.old.build.sh',
],
'Must read correct value.',
);
});
test('only invalid or false values', () => {
assert.deepStrictEqual(
PromptsConfig.promptSourceFolders(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'../assets/IMG/logo.v2.png': '',
'/mnt/storage/video.archive/episode.01.mkv': false,
'/usr/local/share/.fonts/CustomFont.otf': '',
'./hidden.dir/.subhidden': '\f',
'/opt/Software/v3.2.1/build.log': ' ',
'/var/data/datafile.2025-02-05.json': '\n',
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
[
'.github/prompts',
],
'Must read correct value.',
);
});
test('filters out disabled default location', () => {
assert.deepStrictEqual(
PromptsConfig.promptSourceFolders(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'.github/prompts': false,
'../assets/img/logo.v2.png': true,
'/mnt/storage/video.archive/episode.01.mkv': false,
'../.local/bin/script.sh': true,
'/usr/local/share/.fonts/CustomFont.otf': '',
'../../development/branch.name/some.test': true,
'.giThub/prompts': true,
'/Home/user/.ssh/config': true,
'./hidden.dir/.subhidden': '\f',
'/tmp/.temp.folder/cache.db': true,
'/opt/software/v3.2.1/build.log': ' ',
'': true,
'./scripts/.old.build.sh': true,
'/var/data/datafile.2025-02-05.json': '\n',
'\n\n': true,
'\t': true,
'\v': true,
'\f': true,
'\r\n': true,
'\f\f': true,
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
[
'../assets/img/logo.v2.png',
'../.local/bin/script.sh',
'../../development/branch.name/some.test',
'.giThub/prompts',
'/Home/user/.ssh/config',
'/tmp/.temp.folder/cache.db',
'./scripts/.old.build.sh',
],
'Must read correct value.',
);
});
});
});
});
@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { URI } from '../../../../base/common/uri.js';
import { getCleanPromptName, isPromptFile } from '../../common/constants.js';
import { randomInt } from '../../../../base/common/numbers.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
suite('Prompt Constants', () => {
ensureNoDisposablesAreLeakedInTestSuite();
suite('• getCleanPromptName', () => {
test('• returns a clean prompt name', () => {
assert.strictEqual(
getCleanPromptName(URI.file('/path/to/my-prompt.prompt.md')),
'my-prompt',
);
assert.strictEqual(
getCleanPromptName(URI.file('../common.prompt.md')),
'common',
);
const expectedPromptName = `some-${randomInt(1000)}`;
assert.strictEqual(
getCleanPromptName(URI.file(`./${expectedPromptName}.prompt.md`)),
expectedPromptName,
);
});
test('• throws if not a prompt file URI provided', () => {
assert.throws(() => {
getCleanPromptName(URI.file('/path/to/default.prompt.md1'));
});
assert.throws(() => {
getCleanPromptName(URI.file('./some.md'));
});
assert.throws(() => {
getCleanPromptName(URI.file('../some-folder/frequent.txt'));
});
assert.throws(() => {
getCleanPromptName(URI.file('/etc/prompts/my-prompt'));
});
});
});
suite('• isPromptFile', () => {
test('• returns `true` for prompt files', () => {
assert(
isPromptFile(URI.file('/path/to/my-prompt.prompt.md')),
);
assert(
isPromptFile(URI.file('../common.prompt.md')),
);
assert(
isPromptFile(URI.file(`./some-${randomInt(1000)}.prompt.md`)),
);
});
test('• returns `false` for non-prompt files', () => {
assert(
!isPromptFile(URI.file('/path/to/my-prompt.prompt.md1')),
);
assert(
!isPromptFile(URI.file('../common.md')),
);
assert(
!isPromptFile(URI.file(`./some-${randomInt(1000)}.txt`)),
);
});
});
});
@@ -5,10 +5,10 @@
import assert from 'assert';
import { mockObject } from './mock.js';
import { typeCheck } from '../../../../../../../base/common/types.js';
import { randomInt } from '../../../../../../../base/common/numbers.js';
import { randomBoolean } from '../../../../../../../base/test/common/testUtils.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';
import { typeCheck } from '../../../../../base/common/types.js';
import { randomInt } from '../../../../../base/common/numbers.js';
import { randomBoolean } from '../../../../../base/test/common/testUtils.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
suite('mock', () => {
ensureNoDisposablesAreLeakedInTestSuite();
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { assertOneOf } from '../../../../../../../base/common/types.js';
import { assertOneOf } from '../../../../../base/common/types.js';
/**
* Mocks an `TObject` with the provided `overrides`.
@@ -7,6 +7,7 @@ import { URI } from '../../../../base/common/uri.js';
import { Event } from '../../../../base/common/event.js';
import { VSBuffer } from '../../../../base/common/buffer.js';
import { deepClone } from '../../../../base/common/objects.js';
import { isPromptFile } from '../../../prompts/common/constants.js';
import { IStorageService } from '../../../storage/common/storage.js';
import { ITelemetryService } from '../../../telemetry/common/telemetry.js';
import { IStringDictionary } from '../../../../base/common/collections.js';
@@ -516,7 +517,7 @@ export class PromptsSynchronizer extends AbstractSynchroniser implements IUserDa
for (const entry of stat.children || []) {
const resource = entry.resource;
if (!resource.path.endsWith('.prompt.md')) {
if (!isPromptFile(resource)) {
continue;
}
@@ -12,7 +12,7 @@ import { IModelService } from '../../../editor/common/services/model.js';
import { ITextModelService } from '../../../editor/common/services/resolverService.js';
import { IFileService, FileOperation } from '../../../platform/files/common/files.js';
import { ExtHostContext, ExtHostDocumentsShape, MainThreadDocumentsShape } from '../common/extHost.protocol.js';
import { ITextFileEditorModel, ITextFileService } from '../../services/textfile/common/textfiles.js';
import { EncodingMode, ITextFileEditorModel, ITextFileService, TextFileResolveReason } from '../../services/textfile/common/textfiles.js';
import { IUntitledTextEditorModel } from '../../services/untitled/common/untitledTextEditorModel.js';
import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';
import { toLocalResource, extUri, IExtUri } from '../../../base/common/resources.js';
@@ -219,7 +219,7 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen
return Boolean(target);
}
async $tryOpenDocument(uriData: UriComponents): Promise<URI> {
async $tryOpenDocument(uriData: UriComponents, options?: { encoding?: string }): Promise<URI> {
const inputUri = URI.revive(uriData);
if (!inputUri.scheme || !(inputUri.fsPath || inputUri.authority)) {
throw new ErrorNoTelemetry(`Invalid uri. Scheme and authority or path must be set.`);
@@ -230,11 +230,11 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen
let promise: Promise<URI>;
switch (canonicalUri.scheme) {
case Schemas.untitled:
promise = this._handleUntitledScheme(canonicalUri);
promise = this._handleUntitledScheme(canonicalUri, options);
break;
case Schemas.file:
default:
promise = this._handleAsResourceInput(canonicalUri);
promise = this._handleAsResourceInput(canonicalUri, options);
break;
}
@@ -255,31 +255,40 @@ export class MainThreadDocuments extends Disposable implements MainThreadDocumen
}
}
$tryCreateDocument(options?: { language?: string; content?: string }): Promise<URI> {
return this._doCreateUntitled(undefined, options ? options.language : undefined, options ? options.content : undefined);
$tryCreateDocument(options?: { language?: string; content?: string; encoding?: string }): Promise<URI> {
return this._doCreateUntitled(undefined, options);
}
private async _handleAsResourceInput(uri: URI): Promise<URI> {
private async _handleAsResourceInput(uri: URI, options?: { encoding?: string }): Promise<URI> {
if (options?.encoding) {
const model = await this._textFileService.files.resolve(uri, { encoding: options.encoding, reason: TextFileResolveReason.REFERENCE });
if (model.isDirty()) {
throw new ErrorNoTelemetry(`Cannot re-open a dirty text document with different encoding. Save it first.`);
}
await model.setEncoding(options.encoding, EncodingMode.Decode);
}
const ref = await this._textModelResolverService.createModelReference(uri);
this._modelReferenceCollection.add(uri, ref, ref.object.textEditorModel.getValueLength());
return ref.object.textEditorModel.uri;
}
private async _handleUntitledScheme(uri: URI): Promise<URI> {
private async _handleUntitledScheme(uri: URI, options?: { encoding?: string }): Promise<URI> {
const asLocalUri = toLocalResource(uri, this._environmentService.remoteAuthority, this._pathService.defaultUriScheme);
const exists = await this._fileService.exists(asLocalUri);
if (exists) {
// don't create a new file ontop of an existing file
return Promise.reject(new Error('file already exists'));
}
return await this._doCreateUntitled(Boolean(uri.path) ? uri : undefined);
return await this._doCreateUntitled(Boolean(uri.path) ? uri : undefined, options);
}
private async _doCreateUntitled(associatedResource?: URI, languageId?: string, initialValue?: string): Promise<URI> {
private async _doCreateUntitled(associatedResource?: URI, options?: { language?: string; content?: string; encoding?: string }): Promise<URI> {
const model = this._textFileService.untitled.create({
associatedResource,
languageId,
initialValue
languageId: options?.language,
initialValue: options?.content,
encoding: options?.encoding
});
const resource = model.resource;
const ref = await this._textModelResolverService.createModelReference(resource);
@@ -211,7 +211,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol));
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels));
const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels, extHostDiagnostics));
const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol));
const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol));
const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol));
@@ -1024,10 +1024,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
set textDocuments(value) {
throw new errors.ReadonlyError('textDocuments');
},
openTextDocument(uriOrFileNameOrOptions?: vscode.Uri | string | { language?: string; content?: string }) {
openTextDocument(uriOrFileNameOrOptions?: vscode.Uri | string | { language?: string; content?: string; encoding?: string }, options?: { encoding?: string }) {
let uriPromise: Thenable<URI>;
const options = uriOrFileNameOrOptions as { language?: string; content?: string };
options = (options ?? uriOrFileNameOrOptions) as ({ language?: string; content?: string; encoding?: string } | undefined);
if (typeof options?.encoding === 'string') {
checkProposedApiEnabled(extension, 'textDocumentEncoding');
}
if (typeof uriOrFileNameOrOptions === 'string') {
uriPromise = Promise.resolve(URI.file(uriOrFileNameOrOptions));
} else if (URI.isUri(uriOrFileNameOrOptions)) {
@@ -1043,7 +1047,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
if (uri.scheme === Schemas.vscodeRemote && !uri.authority) {
extHostApiDeprecation.report('workspace.openTextDocument', extension, `A URI of 'vscode-remote' scheme requires an authority.`);
}
return extHostDocuments.ensureDocumentData(uri).then(documentData => {
return extHostDocuments.ensureDocumentData(uri, options).then(documentData => {
return documentData.document;
});
});
@@ -1544,6 +1548,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
ChatResultFeedbackKind: extHostTypes.ChatResultFeedbackKind,
ChatVariableLevel: extHostTypes.ChatVariableLevel,
ChatCompletionItem: extHostTypes.ChatCompletionItem,
ChatReferenceDiagnostic: extHostTypes.ChatReferenceDiagnostic,
CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall,
CallHierarchyItem: extHostTypes.CallHierarchyItem,
CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall,
@@ -238,8 +238,8 @@ export interface MainThreadDocumentContentProvidersShape extends IDisposable {
}
export interface MainThreadDocumentsShape extends IDisposable {
$tryCreateDocument(options?: { language?: string; content?: string }): Promise<UriComponents>;
$tryOpenDocument(uri: UriComponents): Promise<UriComponents>;
$tryCreateDocument(options?: { language?: string; content?: string; encoding?: string }): Promise<UriComponents>;
$tryOpenDocument(uri: UriComponents, options?: { encoding?: string }): Promise<UriComponents>;
$trySaveDocument(uri: UriComponents): Promise<boolean>;
}
@@ -32,6 +32,7 @@ import * as typeConvert from './extHostTypeConverters.js';
import * as extHostTypes from './extHostTypes.js';
import { isChatViewTitleActionContext } from '../../contrib/chat/common/chatActions.js';
import { IChatRelatedFile, IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
import { ExtHostDiagnostics } from './extHostDiagnostics.js';
class ChatAgentResponseStream {
@@ -324,7 +325,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
private readonly _logService: ILogService,
private readonly _commands: ExtHostCommands,
private readonly _documents: ExtHostDocuments,
private readonly _languageModels: ExtHostLanguageModels
private readonly _languageModels: ExtHostLanguageModels,
private readonly _diagnostics: ExtHostDiagnostics,
) {
super();
this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2);
@@ -402,7 +404,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
const { request, location, history } = await this._createRequest(requestDto, context, detector.extension);
const model = await this.getModelForRequest(request, detector.extension);
const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(detector.extension, 'chatReadonlyPromptReference'));
const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(detector.extension, 'chatReadonlyPromptReference'), this.getDiagnosticsWhenEnabled(detector.extension));
return detector.provider.provideParticipantDetection(
extRequest,
@@ -486,7 +488,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables);
const model = await this.getModelForRequest(request, agent.extension);
const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(agent.extension, 'chatReadonlyPromptReference'));
const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, isProposedApiEnabled(agent.extension, 'chatReadonlyPromptReference'), this.getDiagnosticsWhenEnabled(agent.extension));
inFlightRequest = { requestId: requestDto.requestId, extRequest };
this._inFlightRequests.add(inFlightRequest);
@@ -538,6 +540,13 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
}
}
private getDiagnosticsWhenEnabled(extension: Readonly<IRelaxedExtensionDescription>) {
if (!isProposedApiEnabled(extension, 'chatReferenceDiagnostic')) {
return [];
}
return this._diagnostics.getDiagnostics();
}
private async prepareHistoryTurns(extension: Readonly<IRelaxedExtensionDescription>, agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> {
const res: (vscode.ChatRequestTurn | vscode.ChatResponseTurn)[] = [];
@@ -551,7 +560,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
const hasReadonlyProposal = isProposedApiEnabled(extension, 'chatReadonlyPromptReference');
const varsWithoutTools = h.request.variables.variables
.filter(v => !v.isTool)
.map(v => typeConvert.ChatPromptReference.to(v, hasReadonlyProposal));
.map(v => typeConvert.ChatPromptReference.to(v, hasReadonlyProposal, this.getDiagnosticsWhenEnabled(extension)));
const toolReferences = h.request.variables.variables
.filter(v => v.isTool)
.map(typeConvert.ChatLanguageModelToolReference.to);
@@ -76,16 +76,16 @@ export class ExtHostDocuments implements ExtHostDocumentsShape {
return data.document;
}
public ensureDocumentData(uri: URI): Promise<ExtHostDocumentData> {
public ensureDocumentData(uri: URI, options?: { encoding?: string }): Promise<ExtHostDocumentData> {
const cached = this._documentsAndEditors.getDocument(uri);
if (cached) {
if (cached && (!options?.encoding || cached.document.encoding === options.encoding)) {
return Promise.resolve(cached);
}
let promise = this._documentLoader.get(uri.toString());
if (!promise) {
promise = this._proxy.$tryOpenDocument(uri).then(uriData => {
promise = this._proxy.$tryOpenDocument(uri, options).then(uriData => {
this._documentLoader.delete(uri.toString());
const canonicalUri = URI.revive(uriData);
return assertIsDefined(this._documentsAndEditors.getDocument(canonicalUri));
@@ -94,12 +94,21 @@ export class ExtHostDocuments implements ExtHostDocumentsShape {
return Promise.reject(err);
});
this._documentLoader.set(uri.toString(), promise);
} else {
if (options?.encoding) {
promise = promise.then(data => {
if (data.document.encoding !== options.encoding) {
return this.ensureDocumentData(uri, options);
}
return data;
});
}
}
return promise;
}
public createDocumentData(options?: { language?: string; content?: string }): Promise<URI> {
public createDocumentData(options?: { language?: string; content?: string; encoding?: string }): Promise<URI> {
return this._proxy.$tryCreateDocument(options).then(data => URI.revive(data));
}
@@ -2761,7 +2761,7 @@ export namespace ChatResponsePart {
}
export namespace ChatAgentRequest {
export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, hasReadonlyProposal: boolean): vscode.ChatRequest {
export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, hasReadonlyProposal: boolean, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][]): vscode.ChatRequest {
const toolReferences = request.variables.variables.filter(v => v.isTool);
const variableReferences = request.variables.variables.filter(v => !v.isTool);
return {
@@ -2770,7 +2770,7 @@ export namespace ChatAgentRequest {
attempt: request.attempt ?? 0,
enableCommandDetection: request.enableCommandDetection ?? true,
isParticipantDetected: request.isParticipantDetected ?? false,
references: variableReferences.map(v => ChatPromptReference.to(v, hasReadonlyProposal)),
references: variableReferences.map(v => ChatPromptReference.to(v, hasReadonlyProposal, diagnostics)),
toolReferences: toolReferences.map(ChatLanguageModelToolReference.to),
location: ChatLocation.to(request.location),
acceptedConfirmationData: request.acceptedConfirmationData,
@@ -2814,19 +2814,48 @@ export namespace ChatLocation {
}
export namespace ChatPromptReference {
export function to(variable: IChatRequestVariableEntry, hasReadonlyProposal: boolean): vscode.ChatPromptReference {
const value = variable.value;
export function to(variable: IChatRequestVariableEntry, hasReadonlyProposal: boolean, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][]): vscode.ChatPromptReference {
let value: vscode.ChatPromptReference['value'] = variable.value;
if (!value) {
throw new Error('Invalid value reference');
}
if (isUriComponents(value)) {
value = URI.revive(value);
} else if (value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri)) {
value = Location.to(revive(value));
} else if (variable.isImage) {
value = new types.ChatReferenceBinaryData(
variable.mimeType ?? 'image/png',
() => Promise.resolve(new Uint8Array(Object.values(variable.value as number[]))),
variable.references && URI.isUri(variable.references[0].reference) ? variable.references[0].reference : undefined
);
} else if (variable.kind === 'diagnostic') {
const filterSeverity = variable.filterSeverity && DiagnosticSeverity.to(variable.filterSeverity);
const filterUri = variable.filterUri && URI.revive(variable.filterUri).toString();
value = new types.ChatReferenceDiagnostic(diagnostics.map(([uri, d]): [vscode.Uri, vscode.Diagnostic[]] => {
if (variable.filterUri && uri.toString() !== filterUri) {
return [uri, []];
}
return [uri, d.filter(d => {
if (filterSeverity && d.severity > filterSeverity) {
return false;
}
if (variable.filterRange && !editorRange.Range.areIntersectingOrTouching(variable.filterRange, Range.from(d.range))) {
return false;
}
return true;
})];
}).filter(([, d]) => d.length > 0));
}
return {
id: variable.id,
name: variable.name,
range: variable.range && [variable.range.start, variable.range.endExclusive],
value: isUriComponents(value) ? URI.revive(value) :
value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri) ?
Location.to(revive(value)) : variable.isImage ? new types.ChatReferenceBinaryData(variable.mimeType ?? 'image/png', () => Promise.resolve(new Uint8Array(Object.values(value))), variable.references && URI.isUri(variable.references[0].reference) ? variable.references[0].reference : undefined) : value,
value,
modelDescription: variable.modelDescription,
isReadonly: hasReadonlyProposal ? variable.isMarkedReadonly : undefined,
};
@@ -4716,6 +4716,10 @@ export class ChatReferenceBinaryData implements vscode.ChatReferenceBinaryData {
}
}
export class ChatReferenceDiagnostic implements vscode.ChatReferenceDiagnostic {
constructor(public readonly diagnostics: [vscode.Uri, vscode.Diagnostic[]][]) { }
}
export enum LanguageModelChatMessageRole {
User = 1,
Assistant = 2,
+7 -1
View File
@@ -646,18 +646,24 @@ export class ResourceListDnDHandler<T> implements IListDragAndDrop<T> {
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
const resources: URI[] = [];
for (const element of (data as ElementsDragAndDropData<T>).elements) {
const elements = (data as ElementsDragAndDropData<T>).elements;
for (const element of elements) {
const resource = this.toResource(element);
if (resource) {
resources.push(resource);
}
}
this.onWillDragElements(elements, originalEvent);
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent));
}
}
protected onWillDragElements(elements: readonly T[], originalEvent: DragEvent): void {
// noop
}
onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
return false;
}
@@ -837,7 +837,7 @@ export class MultiEditorTabsControl extends EditorTabsControl {
}
});
const tabActionBarDisposable = combinedDisposable(tabActionBar, tabActionListener, toDisposable(insert(this.tabActionBars, tabActionBar)));
const tabActionBarDisposable = combinedDisposable(tabActionRunner, tabActionBar, tabActionListener, toDisposable(insert(this.tabActionBars, tabActionBar)));
// Tab Fade Hider
// Hides the tab fade to the right when tab action left and sizing shrink/fixed, ::after, ::before are already used
@@ -169,6 +169,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl
}
actionRunner.run(primaryAction, notification);
notification.close();
actionRunner.dispose();
}
});
@@ -491,7 +491,7 @@ export class NotificationTemplateRenderer extends Disposable {
if (notification.expanded && isNonEmptyArray(primaryActions)) {
const that = this;
const actionRunner: IActionRunner = new class extends ActionRunner {
const actionRunner: IActionRunner = this.inputDisposables.add(new class extends ActionRunner {
protected override async runAction(action: IAction): Promise<void> {
// Run action
@@ -502,7 +502,7 @@ export class NotificationTemplateRenderer extends Disposable {
notification.close();
}
}
}();
}());
const buttonToolbar = this.inputDisposables.add(new ButtonBar(this.template.buttonsContainer));
for (let i = 0; i < primaryActions.length; i++) {
@@ -332,7 +332,9 @@ export class StatusbarViewModel extends Disposable {
// Fill relative entries to LEFT
if (relativeEntries) {
sortedEntries.push(...relativeEntries.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.LEFT));
sortedEntries.push(...relativeEntries
.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.LEFT)
.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));
}
// Fill referenced entry
@@ -340,7 +342,9 @@ export class StatusbarViewModel extends Disposable {
// Fill relative entries to RIGHT
if (relativeEntries) {
sortedEntries.push(...relativeEntries.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.RIGHT));
sortedEntries.push(...relativeEntries
.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.RIGHT)
.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));
}
// Delete from map to mark as handled
@@ -350,7 +354,7 @@ export class StatusbarViewModel extends Disposable {
// Finally, just append all entries that reference another entry
// that does not exist to the end of the list
for (const [, entries] of mapEntryWithRelativePriority) {
sortedEntries.push(...entries.values());
sortedEntries.push(...Array.from(entries.values()).sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));
}
}
@@ -7,12 +7,13 @@ import { IChatWidget } from '../../chat.js';
import { localize } from '../../../../../../nls.js';
import { URI } from '../../../../../../base/common/uri.js';
import { WithUriValue } from '../../../../../../base/common/types.js';
import { dirname, extUri } from '../../../../../../base/common/resources.js';
import { DOCUMENTATION_URL } from '../../../common/promptSyntax/constants.js';
import { isLinux, isWindows } from '../../../../../../base/common/platform.js';
import { ILabelService } from '../../../../../../platform/label/common/label.js';
import { IOpenerService } from '../../../../../../platform/opener/common/opener.js';
import { basename, dirname, extUri } from '../../../../../../base/common/resources.js';
import { IPromptPath, IPromptsService } from '../../../common/promptSyntax/service/types.js';
import { DOCUMENTATION_URL, PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/constants.js';
import { getCleanPromptName } from '../../../../../../platform/prompts/common/constants.js';
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
import { IPickOptions, IQuickInputService, IQuickPickItem } from '../../../../../../platform/quickinput/common/quickInput.js';
@@ -66,8 +67,7 @@ const createPickItem = (
{ uri }: IPromptPath,
labelService: ILabelService,
): WithUriValue<IQuickPickItem> => {
const fileBasename = basename(uri);
const fileWithoutExtension = fileBasename.replace(PROMPT_FILE_EXTENSION, '');
const fileWithoutExtension = getCleanPromptName(uri);
return {
type: 'item',
@@ -450,8 +450,9 @@ export function registerChatCodeBlockActions() {
const focused = !widget.inputEditor.hasWidgetFocus() && widget.getFocus();
const focusedResponse = isResponseVM(focused) ? focused : undefined;
const currentResponse = curCodeBlockInfo ?
curCodeBlockInfo.element :
const elementId = curCodeBlockInfo?.elementId;
const element = elementId ? widget.viewModel?.getItems().find(item => item.id === elementId) : undefined;
const currentResponse = element ??
(focusedResponse ?? widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item)));
if (!currentResponse || !isResponseVM(currentResponse)) {
return;
@@ -531,8 +532,9 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor):
return;
}
const element = widget?.viewModel?.getItems().find(item => item.id === codeBlockInfo.elementId);
return {
element: codeBlockInfo.element,
element,
codeBlockIndex: codeBlockInfo.codeBlockIndex,
code: editor.getValue(),
languageId: editor.getModel()!.getLanguageId(),
@@ -48,15 +48,15 @@ import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../searc
import { SearchContext } from '../../../search/common/constants.js';
import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { IChatRequestVariableEntry } from '../../common/chatModel.js';
import { IChatEditingService } from '../../common/chatEditingService.js';
import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js';
import { ChatRequestAgentPart } from '../../common/chatParserTypes.js';
import { IChatVariablesService } from '../../common/chatVariables.js';
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView, showEditsView } from '../chat.js';
import { imageToHash, isImage } from '../chatPasteProviders.js';
import { isQuickChat } from '../chatWidget.js';
import { createFolderQuickPick } from '../contrib/chatDynamicVariables.js';
import { createFolderQuickPick, createMarkersQuickPick } from '../contrib/chatDynamicVariables.js';
import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js';
import { resizeImage } from '../imageUtils.js';
import { CHAT_CATEGORY } from './chatActions.js';
@@ -75,7 +75,7 @@ export function registerChatContextActions() {
*/
type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem |
IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem |
IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IPromptInstructionsQuickPickItem | IFolderQuickPickItem;
IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IPromptInstructionsQuickPickItem | IFolderQuickPickItem | IDiagnosticsQuickPickItem;
/**
* These are the types that we can get out of the quick pick
@@ -103,6 +103,12 @@ function isIFolderSearchResultQuickPickItem(obj: unknown): obj is IFolderResultQ
&& (obj as IFolderResultQuickPickItem).kind === 'folder-search-result');
}
function isIDiagnosticsQuickPickItemWithFilter(obj: unknown): obj is IDiagnosticsQuickPickItemWithFilter {
return (
typeof obj === 'object'
&& (obj as IDiagnosticsQuickPickItemWithFilter).kind === 'diagnostic-filter');
}
function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource {
return (
typeof obj === 'object'
@@ -210,6 +216,19 @@ interface IScreenShotQuickPickItem extends IQuickPickItem {
icon?: ThemeIcon;
}
interface IDiagnosticsQuickPickItem extends IQuickPickItem {
kind: 'diagnostic';
id: string;
icon?: ThemeIcon;
}
interface IDiagnosticsQuickPickItemWithFilter extends IQuickPickItem {
kind: 'diagnostic-filter';
id: string;
filter: IDiagnosticVariableEntryFilterData;
icon?: ThemeIcon;
}
/**
* Quick pick item for prompt instructions attachment.
*/
@@ -445,13 +464,13 @@ export class AttachContextAction extends Action2 {
category: CHAT_CATEGORY,
precondition: ContextKeyExpr.or(AttachContextAction._cdt, ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession))),
keybinding: {
when: ChatContextKeys.inChatInput,
when: ContextKeyExpr.and(ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), ChatContextKeys.inChatInput),
primary: KeyMod.CtrlCmd | KeyCode.Slash,
weight: KeybindingWeight.EditorContrib
},
menu: [
{
when: ContextKeyExpr.or(ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), AttachContextAction._cdt)),
when: AttachContextAction._cdt,
id: MenuId.ChatInput,
group: 'navigation',
order: 2
@@ -499,6 +518,14 @@ export class AttachContextAction extends Action2 {
isFile: false,
isDirectory: true,
});
} else if (isIDiagnosticsQuickPickItemWithFilter(pick)) {
toAttach.push({
id: pick.id,
name: pick.label,
value: pick.filter,
kind: 'diagnostic',
...pick.filter,
});
} else if (isIQuickPickItemWithResource(pick) && pick.resource) {
if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) {
// checks if the file is an image
@@ -515,26 +542,21 @@ export class AttachContextAction extends Action2 {
});
}
} else {
// file attachment
if (chatEditingService) {
getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(pick.resource);
} else {
let isOmitted = false;
try {
const createdModel = await textModelService.createModelReference(pick.resource);
createdModel.dispose();
} catch {
isOmitted = true;
}
toAttach.push({
id: this._getFileContextId({ resource: pick.resource }),
value: pick.resource,
name: pick.label,
isFile: true,
isOmitted
});
let isOmitted = false;
try {
const createdModel = await textModelService.createModelReference(pick.resource);
createdModel.dispose();
} catch {
isOmitted = true;
}
toAttach.push({
id: this._getFileContextId({ resource: pick.resource }),
value: pick.resource,
name: pick.label,
isFile: true,
isOmitted
});
}
} else if (isIGotoSymbolQuickPickItem(pick) && pick.uri && pick.range) {
toAttach.push({
@@ -548,31 +570,23 @@ export class AttachContextAction extends Action2 {
for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput || e instanceof NotebookEditorInput)) {
const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource;
if (uri) {
if (chatEditingService) {
getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(uri);
} else {
toAttach.push({
id: this._getFileContextId({ resource: uri }),
value: uri,
name: labelService.getUriBasenameLabel(uri),
isFile: true,
});
}
toAttach.push({
id: this._getFileContextId({ resource: uri }),
value: uri,
name: labelService.getUriBasenameLabel(uri),
isFile: true,
});
}
}
} else if (isISearchResultsQuickPickItem(pick)) {
const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView;
for (const result of searchView.model.searchResult.matches()) {
if (chatEditingService) {
getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(result.resource);
} else {
toAttach.push({
id: this._getFileContextId({ resource: result.resource }),
value: result.resource,
name: labelService.getUriBasenameLabel(result.resource),
isFile: true,
});
}
toAttach.push({
id: this._getFileContextId({ resource: result.resource }),
value: result.resource,
name: labelService.getUriBasenameLabel(result.resource),
isFile: true,
});
}
} else if (isRelatedFileQuickPickItem(pick)) {
// Get all provider results and show them in a second tier picker
@@ -580,12 +594,12 @@ export class AttachContextAction extends Action2 {
if (!chatSessionId || !chatEditingService) {
continue;
}
const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), CancellationToken.None);
const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None);
if (!relatedFiles) {
continue;
}
const attachments = widget.attachmentModel.getAttachmentIDs();
const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), CancellationToken.None)
const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None)
.then((files) => (files ?? []).reduce<(WithUriValue<IQuickPickItem> | IQuickPickSeparator)[]>((acc, cur) => {
acc.push({ type: 'separator', label: cur.group });
for (const file of cur.files) {
@@ -685,103 +699,110 @@ export class AttachContextAction extends Action2 {
const chatEditingService = widget.location === ChatAgentLocation.EditingSession ? accessor.get(IChatEditingService) : undefined;
const quickPickItems: IAttachmentQuickPickItem[] = [];
if (!context || !context.showFilesOnly) {
if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) {
const imageData = await clipboardService.readImage();
if (isImage(imageData)) {
quickPickItems.push({
kind: 'image',
id: await imageToHash(imageData),
label: localize('imageFromClipboard', 'Image from Clipboard'),
iconClass: ThemeIcon.asClassName(Codicon.fileMedia),
});
}
if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) {
const imageData = await clipboardService.readImage();
if (isImage(imageData)) {
quickPickItems.push({
kind: 'screenshot',
id: ScreenshotVariableId,
icon: ThemeIcon.fromId(Codicon.deviceCamera.id),
iconClass: ThemeIcon.asClassName(Codicon.deviceCamera),
label: (isElectron
? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window')
: localize('chatContext.attachScreenshot.labelWeb', 'Screenshot')),
kind: 'image',
id: await imageToHash(imageData),
label: localize('imageFromClipboard', 'Image from Clipboard'),
iconClass: ThemeIcon.asClassName(Codicon.fileMedia),
});
}
if (widget.viewModel?.sessionId) {
const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart);
if (agentPart) {
const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None);
for (const variable of completions) {
if (variable.fullName && variable.command) {
quickPickItems.push({
kind: 'command',
label: variable.fullName,
id: variable.id,
command: variable.command,
icon: variable.icon,
iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined,
value: variable.value,
name: variable.name
});
} else {
// Currently there's nothing that falls into this category
}
quickPickItems.push({
kind: 'screenshot',
id: ScreenshotVariableId,
icon: ThemeIcon.fromId(Codicon.deviceCamera.id),
iconClass: ThemeIcon.asClassName(Codicon.deviceCamera),
label: (isElectron
? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window')
: localize('chatContext.attachScreenshot.labelWeb', 'Screenshot')),
});
}
if (widget.viewModel?.sessionId) {
const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart);
if (agentPart) {
const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None);
for (const variable of completions) {
if (variable.fullName && variable.command) {
quickPickItems.push({
kind: 'command',
label: variable.fullName,
id: variable.id,
command: variable.command,
icon: variable.icon,
iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined,
value: variable.value,
name: variable.name
});
} else {
// Currently there's nothing that falls into this category
}
}
}
}
for (const tool of languageModelToolsService.getTools()) {
if (tool.canBeReferencedInPrompt) {
const item: IToolQuickPickItem = {
kind: 'tool',
label: tool.displayName ?? '',
id: tool.id,
icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined // TODO need to support icon path?
};
if (ThemeIcon.isThemeIcon(tool.icon)) {
item.iconClass = ThemeIcon.asClassName(tool.icon);
} else if (tool.icon) {
item.iconPath = tool.icon;
}
quickPickItems.push(item);
for (const tool of languageModelToolsService.getTools()) {
if (tool.canBeReferencedInPrompt) {
const item: IToolQuickPickItem = {
kind: 'tool',
label: tool.displayName ?? '',
id: tool.id,
icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined // TODO need to support icon path?
};
if (ThemeIcon.isThemeIcon(tool.icon)) {
item.iconClass = ThemeIcon.asClassName(tool.icon);
} else if (tool.icon) {
item.iconPath = tool.icon;
}
quickPickItems.push(item);
}
}
quickPickItems.push({
kind: 'quickaccess',
label: localize('chatContext.symbol', 'Symbol...'),
iconClass: ThemeIcon.asClassName(Codicon.symbolField),
prefix: SymbolsQuickAccessProvider.PREFIX,
id: 'symbol'
});
quickPickItems.push({
kind: 'folder',
label: localize('chatContext.folder', 'Folder...'),
iconClass: ThemeIcon.asClassName(Codicon.folder),
id: 'folder',
});
quickPickItems.push({
kind: 'diagnostic',
label: localize('chatContext.diagnstic', 'Problem...'),
iconClass: ThemeIcon.asClassName(Codicon.error),
id: 'diagnostic'
});
if (widget.location === ChatAgentLocation.Notebook) {
quickPickItems.push({
kind: 'quickaccess',
label: localize('chatContext.symbol', 'Symbol...'),
iconClass: ThemeIcon.asClassName(Codicon.symbolField),
prefix: SymbolsQuickAccessProvider.PREFIX,
id: 'symbol'
kind: 'command',
id: 'chatContext.notebook.kernelVariable',
icon: ThemeIcon.fromId(Codicon.serverEnvironment.id),
iconClass: ThemeIcon.asClassName(Codicon.serverEnvironment),
value: 'kernelVariable',
label: localize('chatContext.notebook.kernelVariable', 'Kernel Variable...'),
command: {
id: 'notebook.chat.selectAndInsertKernelVariable',
title: localize('chatContext.notebook.selectkernelVariable', 'Select and Insert Kernel Variable'),
arguments: [{ widget, range: undefined }]
}
});
}
quickPickItems.push({
kind: 'folder',
label: localize('chatContext.folder', 'Folder...'),
iconClass: ThemeIcon.asClassName(Codicon.folder),
id: 'folder',
});
if (widget.location === ChatAgentLocation.Notebook) {
quickPickItems.push({
kind: 'command',
id: 'chatContext.notebook.kernelVariable',
icon: ThemeIcon.fromId(Codicon.serverEnvironment.id),
iconClass: ThemeIcon.asClassName(Codicon.serverEnvironment),
value: 'kernelVariable',
label: localize('chatContext.notebook.kernelVariable', 'Kernel Variable...'),
command: {
id: 'notebook.chat.selectAndInsertKernelVariable',
title: localize('chatContext.notebook.selectkernelVariable', 'Select and Insert Kernel Variable'),
arguments: [{ widget, range: undefined }]
}
});
}
} else if (context.showFilesOnly) {
if (context?.showFilesOnly) {
if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || (getEditingSession(chatEditingService, widget)?.workingSet.size))) {
quickPickItems.push({
quickPickItems.unshift({
kind: 'related-files',
id: 'related-files',
label: localize('chatContext.relatedFiles', 'Related Files'),
@@ -789,7 +810,7 @@ export class AttachContextAction extends Action2 {
});
}
if (editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0) {
quickPickItems.push({
quickPickItems.unshift({
kind: 'open-editors',
id: 'open-editors',
label: localize('chatContext.editors', 'Open Editors'),
@@ -797,7 +818,7 @@ export class AttachContextAction extends Action2 {
});
}
if (SearchContext.HasSearchResults.getValue(contextKeyService)) {
quickPickItems.push({
quickPickItems.unshift({
kind: 'search-results',
id: 'search-results',
label: localize('chatContext.searchResults', 'Search Results'),
@@ -827,6 +848,9 @@ export class AttachContextAction extends Action2 {
this._show(quickInputService, commandService, widget, quickChatService, quickPickItems.sort(function (a, b) {
if (a.kind === 'open-editors') { return -1; }
if (b.kind === 'open-editors') { return 1; }
const first = extractTextFromIconLabel(a.label).toUpperCase();
const second = extractTextFromIconLabel(b.label).toUpperCase();
@@ -834,16 +858,33 @@ export class AttachContextAction extends Action2 {
}), clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', context?.placeholder);
}
private async _showDiagnosticsPick(instantiationService: IInstantiationService): Promise<IDiagnosticsQuickPickItemWithFilter | undefined> {
const filter = await instantiationService.invokeFunction(accessor => createMarkersQuickPick(accessor));
if (!filter) {
return undefined;
}
return {
kind: 'diagnostic-filter',
id: IDiagnosticVariableEntryFilterData.id(filter),
label: IDiagnosticVariableEntryFilterData.label(filter),
filter,
};
}
private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickChatService: IQuickChatService, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, fileService: IFileService, textModelService: ITextModelService, instantiationService: IInstantiationService, query: string = '', placeholder?: string) {
const providerOptions: AnythingQuickAccessProviderRunOptions = {
handleAccept: async (item: IChatContextQuickPickItem, isBackgroundAccept: boolean) => {
handleAccept: async (inputItem: IChatContextQuickPickItem, isBackgroundAccept: boolean) => {
let item: IChatContextQuickPickItem | undefined = inputItem;
if ('kind' in item && item.kind === 'folder') {
const folderItem = await this._showFolders(instantiationService);
if (!folderItem) {
this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', placeholder);
return;
}
item = folderItem;
item = await this._showFolders(instantiationService);
} else if ('kind' in item && item.kind === 'diagnostic') {
item = await this._showDiagnosticsPick(instantiationService);
}
if (!item) {
this._show(quickInputService, commandService, widget, quickChatService, quickPickItems, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, fileService, textModelService, instantiationService, '', placeholder);
return;
}
if ('prefix' in item) {
@@ -862,13 +903,6 @@ export class AttachContextAction extends Action2 {
filter: (item: IChatContextQuickPickItem | IQuickPickSeparator) => {
// Avoid attaching the same context twice
const attachedContext = widget.attachmentModel.getAttachmentIDs();
if (chatEditingService) {
for (const [file, state] of getEditingSession(chatEditingService, widget)?.workingSet.entries() ?? []) {
if (state.state !== WorkingSetEntryState.Suggested) {
attachedContext.add(this._getFileContextId({ resource: file }));
}
}
}
if (isIOpenEditorsQuickPickItem(item)) {
for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput)) {
@@ -935,16 +969,21 @@ registerAction2(class AttachFilesAction extends AttachContextAction {
constructor() {
super({
id: 'workbench.action.chat.editing.attachFiles',
title: localize2('workbench.action.chat.editing.attachFiles.label', "Add Files to Working Set"),
title: localize2('workbench.action.chat.editing.attachFiles.label', "Add Files to Copilot Edits"),
f1: false,
category: CHAT_CATEGORY,
precondition: ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)
precondition: ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession),
keybinding: {
when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)),
primary: KeyMod.CtrlCmd | KeyCode.Slash,
weight: KeybindingWeight.EditorContrib
}
});
}
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
const context = args[0];
const attachFilesContext = { ...context, showFilesOnly: true, placeholder: localize('chatAttachFiles', 'Search for files to add to your working set') };
const attachFilesContext = { ...context, showFilesOnly: true };
return super.run(accessor, attachFilesContext);
}
});
@@ -501,8 +501,9 @@ class SendToNewChatAction extends Action2 {
}
}
export const CancelChatActionId = 'workbench.action.chat.cancel';
export class CancelAction extends Action2 {
static readonly ID = 'workbench.action.chat.cancel';
static readonly ID = CancelChatActionId;
constructor() {
super({
id: CancelAction.ID,
@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
import { localize2 } from '../../../../../nls.js';
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { IChatToolInvocation } from '../../common/chatService.js';
import { isResponseVM } from '../../common/chatViewModel.js';
import { IChatWidgetService } from '../chat.js';
import { CHAT_CATEGORY } from './chatActions.js';
export const AcceptToolConfirmationActionId = 'workbench.action.chat.acceptTool';
class AcceptToolConfirmation extends Action2 {
constructor() {
super({
id: AcceptToolConfirmationActionId,
title: localize2('chat.accept', "Accept"),
f1: false,
category: CHAT_CATEGORY,
keybinding: {
when: ChatContextKeys.inChatInput,
primary: KeyMod.CtrlCmd | KeyCode.Enter,
weight: KeybindingWeight.EditorContrib
},
});
}
run(accessor: ServicesAccessor, ...args: any[]) {
const chatWidgetService = accessor.get(IChatWidgetService);
const lastItem = chatWidgetService.lastFocusedWidget?.viewModel?.getItems().at(-1);
if (!isResponseVM(lastItem)) {
return;
}
const unconfirmedToolInvocation = lastItem.model.response.value.find((item): item is IChatToolInvocation => item.kind === 'toolInvocation' && !item.isConfirmed);
if (unconfirmedToolInvocation) {
unconfirmedToolInvocation.confirmed.complete(true);
}
}
}
export function registerChatToolActions() {
registerAction2(AcceptToolConfirmation);
}
@@ -17,11 +17,11 @@ import { ILabelService } from '../../../../../../platform/label/common/label.js'
import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js';
import { IModelService } from '../../../../../../editor/common/services/model.js';
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
import { PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/constants.js';
import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js';
import { ILanguageService } from '../../../../../../editor/common/languages/language.js';
import { FileKind, IFileService } from '../../../../../../platform/files/common/files.js';
import { IMenuService, MenuId } from '../../../../../../platform/actions/common/actions.js';
import { getCleanPromptName } from '../../../../../../platform/prompts/common/constants.js';
import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js';
import { ChatInstructionsAttachmentModel } from '../../chatAttachmentModel/chatInstructionsAttachment.js';
@@ -132,7 +132,7 @@ export class InstructionsAttachmentWidget extends Disposable {
title += `\n-\n[${errorCaption}]: ${details}`;
}
const fileWithoutExtension = fileBasename.replace(PROMPT_FILE_EXTENSION, '');
const fileWithoutExtension = getCleanPromptName(file);
label.setFile(URI.file(fileWithoutExtension), {
fileKind: FileKind.FILE,
hidePath: true,
@@ -40,7 +40,6 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f
import { ILanguageModelsService, LanguageModelsService } from '../common/languageModels.js';
import { ILanguageModelStatsService, LanguageModelStatsService } from '../common/languageModelStats.js';
import { ILanguageModelToolsService } from '../common/languageModelToolsService.js';
import { PromptFilesConfig } from '../common/promptSyntax/config.js';
import '../common/promptSyntax/languageFeatures/promptLinkProvider.js';
import '../common/promptSyntax/languageFeatures/promptPathAutocompletion.js';
import { PromptsService } from '../common/promptSyntax/service/promptsService.js';
@@ -93,6 +92,10 @@ import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesCon
import { LanguageModelToolsService } from './languageModelToolsService.js';
import { ChatViewsWelcomeHandler } from './viewsWelcome/chatViewsWelcomeHandler.js';
import { ChatEditingEditorContextKeys } from './chatEditing/chatEditingEditorContextKeys.js';
import { PromptsConfig } from '../../../../platform/prompts/common/config.js';
import { PROMPT_FILE_EXTENSION } from '../../../../platform/prompts/common/constants.js';
import { DOCUMENTATION_URL } from '../common/promptSyntax/constants.js';
import { registerChatToolActions } from './actions/chatToolActions.js';
// Register configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
@@ -162,14 +165,22 @@ configurationRegistry.registerConfiguration({
description: nls.localize('chat.detectParticipant.enabled', "Enables chat participant autodetection for panel chat."),
default: true
},
[PromptFilesConfig.CONFIG_KEY]: {
[PromptsConfig.CONFIG_KEY]: {
type: 'object',
title: PromptFilesConfig.CONFIG_TITLE,
markdownDescription: PromptFilesConfig.CONFIG_DESCRIPTION,
title: nls.localize(
'chat.promptFiles.config.title',
"Prompt Files",
),
markdownDescription: nls.localize(
'chat.promptFiles.config.description',
"Specify location(s) of reusable prompt files (`*{0}`) that can be attached in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.",
PROMPT_FILE_EXTENSION,
DOCUMENTATION_URL,
),
default: {
[PromptFilesConfig.DEFAULT_SOURCE_FOLDER]: false,
[PromptsConfig.DEFAULT_SOURCE_FOLDER]: false,
},
required: [PromptFilesConfig.DEFAULT_SOURCE_FOLDER],
required: [PromptsConfig.DEFAULT_SOURCE_FOLDER],
additionalProperties: { type: 'boolean' },
unevaluatedProperties: { type: 'boolean' },
restricted: true,
@@ -177,10 +188,10 @@ configurationRegistry.registerConfiguration({
tags: ['experimental'],
examples: [
{
[PromptFilesConfig.DEFAULT_SOURCE_FOLDER]: true,
[PromptsConfig.DEFAULT_SOURCE_FOLDER]: true,
},
{
[PromptFilesConfig.DEFAULT_SOURCE_FOLDER]: true,
[PromptsConfig.DEFAULT_SOURCE_FOLDER]: true,
'/Users/vscode/repos/prompts': true,
},
],
@@ -428,6 +439,7 @@ registerNewChatActions();
registerChatContextActions();
registerChatDeveloperActions();
registerChatEditorActions();
registerChatToolActions();
registerEditorFeature(ChatPasteProvidersFeature);
@@ -136,7 +136,7 @@ export interface IChatAccessibilityService {
export interface IChatCodeBlockInfo {
readonly ownerMarkdownPartId: string;
readonly codeBlockIndex: number;
readonly element: ChatTreeItem;
readonly elementId: string;
readonly uri: URI | undefined;
readonly uriPromise: Promise<URI | undefined>;
codemapperUri: URI | undefined;
@@ -190,7 +190,6 @@ export interface IChatWidgetViewOptions {
};
defaultElementHeight?: number;
editorOverflowWidgetsDomNode?: HTMLElement;
enableImplicitContext?: boolean;
enableWorkingSet?: 'explicit' | 'implicit';
}
@@ -42,6 +42,15 @@ export class ChatAttachmentModel extends Disposable {
return this._attachments.size;
}
get fileAttachments(): URI[] {
return this.attachments.reduce<URI[]>((acc, file) => {
if (file.isFile && URI.isUri(file.value)) {
acc.push(file.value);
}
return acc;
}, []);
}
getAttachmentIDs() {
return new Set(this._attachments.keys());
}
@@ -92,48 +101,3 @@ export class ChatAttachmentModel extends Disposable {
this.addContext(...attachments);
}
}
export class EditsAttachmentModel extends ChatAttachmentModel {
private _onFileLimitExceeded = this._register(new Emitter<void>());
readonly onFileLimitExceeded = this._onFileLimitExceeded.event;
get fileAttachments() {
return this.attachments.filter(attachment => attachment.isFile);
}
private readonly _excludedFileAttachments: IChatRequestVariableEntry[] = [];
get excludedFileAttachments(): IChatRequestVariableEntry[] {
return this._excludedFileAttachments;
}
constructor(
@IInstantiationService _initService: IInstantiationService,
) {
super(_initService);
}
override addContext(...attachments: IChatRequestVariableEntry[]) {
const currentAttachmentIds = this.getAttachmentIDs();
const fileAttachments = attachments.filter(attachment => attachment.isFile);
const otherAttachments = attachments.filter(attachment => !attachment.isFile);
// deduplicate file attachments
const newFileAttachments = [];
const newFileAttachmentIds = new Set<string>();
for (const attachment of fileAttachments) {
if (newFileAttachmentIds.has(attachment.id) || currentAttachmentIds.has(attachment.id)) {
continue;
}
newFileAttachmentIds.add(attachment.id);
newFileAttachments.push(attachment);
}
super.addContext(...otherAttachments, ...newFileAttachments);
}
override clear(): void {
this._excludedFileAttachments.splice(0, this._excludedFileAttachments.length);
super.clear();
}
}
@@ -6,7 +6,7 @@
import { URI } from '../../../../../base/common/uri.js';
import { Emitter } from '../../../../../base/common/event.js';
import { IChatRequestVariableEntry } from '../../common/chatModel.js';
import { PromptFilesConfig } from '../../common/promptSyntax/config.js';
import { PromptsConfig } from '../../../../../platform/prompts/common/config.js';
import { IPromptFileReference } from '../../common/promptSyntax/parsers/types.js';
import { ChatInstructionsAttachmentModel } from './chatInstructionsAttachment.js';
import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';
@@ -214,6 +214,6 @@ export class ChatInstructionAttachmentsModel extends Disposable {
* Checks if the prompt instructions feature is enabled in the user settings.
*/
public get featureEnabled(): boolean {
return PromptFilesConfig.enabled(this.configService);
return PromptsConfig.enabled(this.configService);
}
}
@@ -51,12 +51,11 @@ export class ChatAttachmentsContentPart extends Disposable {
private readonly attachedContextDisposables = this._register(new DisposableStore());
private readonly _onDidChangeVisibility = this._register(new Emitter<boolean>());
private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event });
private readonly _contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }));
constructor(
private readonly variables: IChatRequestVariableEntry[],
private readonly contentReferences: ReadonlyArray<IChatContentReference> = [],
private readonly workingSet: ReadonlyArray<URI> = [],
public readonly domNode: HTMLElement | undefined = dom.$('.chat-attached-context'),
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -84,10 +83,6 @@ export class ChatAttachmentsContentPart extends Disposable {
this.variables.forEach(async (attachment) => {
let resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined;
let range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined;
if (resource && attachment.isFile && this.workingSet.find(entry => entry.toString() === resource?.toString())) {
// Don't render attachment if it's in the working set
return;
}
const widget = dom.append(container, dom.$('.chat-attached-context-attachment.show-file-icons'));
const label = this._contextResourceLabels.create(widget, { supportIcons: true, hoverDelegate, hoverTargetOverride: widget });
@@ -16,6 +16,7 @@ import { defaultButtonStyles } from '../../../../../platform/theme/browser/defau
export interface IChatConfirmationButton {
label: string;
isSecondary?: boolean;
tooltip?: string;
data: any;
}
@@ -63,7 +64,7 @@ export class ChatConfirmationWidget extends Disposable {
elements.message.appendChild(renderedMessage.element);
buttons.forEach(buttonData => {
const button = new Button(elements.buttonsContainer, { ...defaultButtonStyles, secondary: buttonData.isSecondary });
const button = this._register(new Button(elements.buttonsContainer, { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip }));
button.label = buttonData.label;
this._register(button.onDidClick(() => this._onDidClick.fire(buttonData)));
});
@@ -130,7 +130,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP
const info: IChatCodeBlockInfo = new class {
readonly ownerMarkdownPartId = ownerMarkdownPartId;
readonly codeBlockIndex = globalIndex;
readonly element = element;
readonly elementId = element.id;
readonly isStreaming = !rendererOptions.renderCodeBlockPills;
codemapperUri = undefined; // will be set async
public get uri() {
@@ -165,7 +165,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP
const info: IChatCodeBlockInfo = new class {
readonly ownerMarkdownPartId = ownerMarkdownPartId;
readonly codeBlockIndex = globalIndex;
readonly element = element;
readonly elementId = element.id;
readonly isStreaming = !isCodeBlockComplete;
readonly codemapperUri = codemapperUri;
public get uri() {
@@ -435,25 +435,15 @@ class CollapsibleListRenderer implements IListRenderer<IChatCollapsibleListItem,
} else if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) {
templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: icon ?? Codicon.globe, title: data.options?.status?.description ?? data.title ?? uri.toString(), strikethrough: data.excluded, extraClasses });
} else {
if (data.state === WorkingSetEntryState.Transient || data.state === WorkingSetEntryState.Suggested) {
templateData.label.setResource(
{
resource: uri,
name: basenameOrAuthority(uri),
description: data.description ?? localize('chat.openEditor', 'Open Editor'),
range: 'range' in reference ? reference.range : undefined,
}, { icon, title: data.options?.status?.description ?? data.title, italic: data.state === WorkingSetEntryState.Suggested, strikethrough: data.excluded, extraClasses });
} else {
templateData.label.setFile(uri, {
fileKind: FileKind.FILE,
// Should not have this live-updating data on a historical reference
fileDecorations: undefined,
range: 'range' in reference ? reference.range : undefined,
title: data.options?.status?.description ?? data.title,
strikethrough: data.excluded,
extraClasses
});
}
templateData.label.setFile(uri, {
fileKind: FileKind.FILE,
// Should not have this live-updating data on a historical reference
fileDecorations: undefined,
range: 'range' in reference ? reference.range : undefined,
title: data.options?.status?.description ?? data.title,
strikethrough: data.excluded,
extraClasses
});
}
}
@@ -12,9 +12,12 @@ import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownR
import { localize } from '../../../../../nls.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
import { IChatProgressMessage, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js';
import { IChatRendererContent } from '../../common/chatViewModel.js';
import { IToolResult } from '../../common/languageModelToolsService.js';
import { CancelChatActionId } from '../actions/chatExecuteActions.js';
import { AcceptToolConfirmationActionId } from '../actions/chatToolActions.js';
import { ChatTreeItem } from '../chat.js';
import { ChatConfirmationWidget } from './chatConfirmationWidget.js';
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
@@ -47,6 +50,7 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa
const partStore = this._register(new DisposableStore());
const render = () => {
dom.clearNode(this.domNode);
partStore.clear();
const subPart = partStore.add(instantiationService.createInstance(ChatToolInvocationSubPart, toolInvocation, context, renderer, listPool));
this.domNode.appendChild(subPart.domNode);
@@ -84,6 +88,7 @@ class ChatToolInvocationSubPart extends Disposable {
listPool: CollapsibleListPool,
@IInstantiationService instantiationService: IInstantiationService,
@IHoverService hoverService: IHoverService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
) {
super();
@@ -106,11 +111,28 @@ class ChatToolInvocationSubPart extends Disposable {
}
const title = toolInvocation.confirmationMessages.title;
const message = toolInvocation.confirmationMessages.message;
const continueLabel = localize('continue', "Continue");
const continueKeybinding = this.keybindingService.lookupKeybinding(AcceptToolConfirmationActionId)?.getLabel();
const continueTooltip = continueKeybinding ? `${continueLabel} (${continueKeybinding})` : continueLabel;
const cancelLabel = localize('cancel', "Cancel");
const cancelKeybinding = this.keybindingService.lookupKeybinding(CancelChatActionId)?.getLabel();
const cancelTooltip = cancelKeybinding ? `${cancelLabel} (${cancelKeybinding})` : cancelLabel;
const confirmWidget = this._register(instantiationService.createInstance(
ChatConfirmationWidget,
title,
message,
[{ label: localize('continue', "Continue"), data: true }, { label: localize('cancel', "Cancel"), data: false, isSecondary: true }]
[
{
label: continueLabel,
data: true,
tooltip: continueTooltip
},
{
label: cancelLabel,
data: false,
isSecondary: true,
tooltip: cancelTooltip
}]
));
this._register(confirmWidget.onDidClick(button => {
toolInvocation.confirmed.complete(button.data);
@@ -11,21 +11,23 @@ import { Codicon } from '../../../../base/common/codicons.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { Mimes } from '../../../../base/common/mime.js';
import { basename, joinPath } from '../../../../base/common/resources.js';
import { Mutable } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { SymbolKinds } from '../../../../editor/common/languages.js';
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { localize } from '../../../../nls.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractSymbolDropData, IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js';
import { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractMarkerDropData, extractSymbolDropData, IDraggedResourceEditorInput, MarkerTransferData } from '../../../../platform/dnd/browser/dnd.js';
import { FileType, IFileService, IFileSystemProvider } from '../../../../platform/files/common/files.js';
import { MarkerSeverity } from '../../../../platform/markers/common/markers.js';
import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';
import { isUntitledResourceEditorInput } from '../../../common/editor.js';
import { EditorInput } from '../../../common/editor/editorInput.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js';
import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../common/chatModel.js';
import { IChatRequestVariableEntry, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry } from '../common/chatModel.js';
import { ChatAttachmentModel } from './chatAttachmentModel.js';
import { IChatInputStyles } from './chatInputPart.js';
import { resizeImage } from './imageUtils.js';
@@ -35,7 +37,8 @@ enum ChatDragAndDropType {
FILE_EXTERNAL,
FOLDER,
IMAGE,
SYMBOL
SYMBOL,
MARKER,
}
export class ChatDragAndDrop extends Themable {
@@ -169,6 +172,8 @@ export class ChatDragAndDrop extends Themable {
return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? ChatDragAndDropType.IMAGE : undefined;
} else if (containsDragType(e, CodeDataTransfers.SYMBOLS)) {
return ChatDragAndDropType.SYMBOL;
} else if (containsDragType(e, CodeDataTransfers.MARKERS)) {
return ChatDragAndDropType.MARKER;
} else if (containsDragType(e, DataTransfers.FILES)) {
return ChatDragAndDropType.FILE_EXTERNAL;
} else if (containsDragType(e, DataTransfers.INTERNAL_URI_LIST)) {
@@ -193,6 +198,7 @@ export class ChatDragAndDrop extends Themable {
case ChatDragAndDropType.FOLDER: return localize('folder', 'Folder');
case ChatDragAndDropType.IMAGE: return localize('image', 'Image');
case ChatDragAndDropType.SYMBOL: return localize('symbol', 'Symbol');
case ChatDragAndDropType.MARKER: return localize('problem', 'Problem');
}
}
@@ -224,6 +230,11 @@ export class ChatDragAndDrop extends Themable {
return [];
}
const markerData = extractMarkerDropData(e);
if (markerData) {
return this.resolveMarkerAttachContext(markerData);
}
if (containsDragType(e, CodeDataTransfers.SYMBOLS)) {
const data = extractSymbolDropData(e);
return this.resolveSymbolsAttachContext(data);
@@ -304,6 +315,33 @@ export class ChatDragAndDrop extends Themable {
});
}
private resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[] {
return markers.map((marker): IDiagnosticVariableEntry => {
const filter: Mutable<IDiagnosticVariableEntryFilterData> = {};
if (!('severity' in marker)) {
filter.filterUri = URI.revive(marker.uri);
filter.filterSeverity = MarkerSeverity.Warning;
} else {
filter.filterUri = URI.revive(marker.resource);
filter.filterSeverity = marker.severity;
filter.filterRange = {
startLineNumber: marker.startLineNumber,
startColumn: marker.startColumn,
endLineNumber: marker.endLineNumber,
endColumn: marker.endColumn
};
}
return {
kind: 'diagnostic',
id: IDiagnosticVariableEntryFilterData.id(filter),
name: IDiagnosticVariableEntryFilterData.label(filter),
value: filter,
...filter,
};
});
}
private setOverlay(target: HTMLElement, type: ChatDragAndDropType | undefined): void {
// Remove any previous overlay text
this.overlayText?.remove();
@@ -33,7 +33,6 @@ import { IChatService } from '../../common/chatService.js';
import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js';
import { CHAT_CATEGORY } from '../actions/chatActions.js';
import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';
import { EditsAttachmentModel } from '../chatAttachmentModel.js';
export interface IEditingSessionActionContext {
widget?: IChatWidget;
@@ -129,7 +128,7 @@ registerAction2(class AddFileToWorkingSet extends WorkingSetAction {
icon: Codicon.plus,
menu: [{
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
when: ContextKeyExpr.or(ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Transient), ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Suggested)),
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Suggested),
order: 0,
group: 'navigation'
}],
@@ -391,8 +390,8 @@ export class ChatEditingRemoveAllFilesAction extends EditingSessionAction {
editingSession.remove(WorkingSetEntryRemovalReason.User, ...uris);
// Remove all file attachments
const fileAttachments = chatWidget.attachmentModel ? [...(chatWidget.attachmentModel as EditsAttachmentModel).excludedFileAttachments, ...(chatWidget.attachmentModel as EditsAttachmentModel).fileAttachments] : [];
const attachmentIdsToRemove = fileAttachments.map(attachment => (attachment.value as URI).toString());
const fileAttachments = chatWidget.attachmentModel ? chatWidget.attachmentModel.fileAttachments : [];
const attachmentIdsToRemove = fileAttachments.map(attachment => attachment.toString());
chatWidget.attachmentModel.delete(...attachmentIdsToRemove);
}
}
@@ -4,32 +4,125 @@
*--------------------------------------------------------------------------------------------*/
import { assert } from '../../../../../base/common/assert.js';
import { decodeBase64, encodeBase64, streamToBuffer, VSBuffer } from '../../../../../base/common/buffer.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { DisposableStore, IReference } from '../../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../../base/common/network.js';
import { ITransaction, IObservable, constObservable } from '../../../../../base/common/observable.js';
import { ITransaction, IObservable, observableValue } from '../../../../../base/common/observable.js';
import { isEqual } from '../../../../../base/common/resources.js';
import { assertType } from '../../../../../base/common/types.js';
import { URI } from '../../../../../base/common/uri.js';
import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js';
import { TextEdit } from '../../../../../editor/common/languages.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IEditorPane } from '../../../../common/editor.js';
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
import { IModifiedFileEntryEditorIntegration } from '../../common/chatEditingService.js';
import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';
import { SnapshotContext } from '../../../../services/workingCopy/common/fileWorkingCopy.js';
import { ChatEditingNotebookFileSystemProvider } from '../../../notebook/browser/contrib/chatEdit/chatEditingNotebookFileSystemProvider.js';
import { NotebookTextModel } from '../../../notebook/common/model/notebookTextModel.js';
import { ICellEditOperation, IResolvedNotebookEditorModel, NotebookData, NotebookSetting, TransientOptions } from '../../../notebook/common/notebookCommon.js';
import { INotebookEditorModelResolverService } from '../../../notebook/common/notebookEditorModelResolverService.js';
import { INotebookService } from '../../../notebook/common/notebookService.js';
import { ChatEditKind, IModifiedFileEntryEditorIntegration } from '../../common/chatEditingService.js';
import { IChatResponseModel } from '../../common/chatModel.js';
import { AbstractChatEditingModifiedFileEntry, ISnapshotEntry } from './chatEditingModifiedFileEntry.js';
import { IChatService } from '../../common/chatService.js';
import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js';
import { ChatEditingSnapshotTextModelContentProvider } from './chatEditingTextModelContentProviders.js';
export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifiedFileEntry {
override originalURI: URI = URI.parse('todo://todo/todo');
private readonly modifiedModel: NotebookTextModel;
private readonly originalModel: NotebookTextModel;
override originalURI: URI;
override initialContent: string;
override initialContent: string = 'JSON.stringify(NotebookData)';
private readonly _changesCount = observableValue<number>(this, 0);
override changesCount: IObservable<number> = this._changesCount;
override changesCount: IObservable<number> = constObservable(Number.MAX_SAFE_INTEGER);
public static async create(uri: URI, _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, telemetryInfo: IModifiedEntryTelemetryInfo, chatKind: ChatEditKind, initialContent: string | undefined, instantiationService: IInstantiationService): Promise<AbstractChatEditingModifiedFileEntry> {
return instantiationService.invokeFunction(async accessor => {
const notebookService = accessor.get(INotebookService);
const resolver = accessor.get(INotebookEditorModelResolverService);
const configurationServie = accessor.get(IConfigurationService);
const resourceRef: IReference<IResolvedNotebookEditorModel> = await resolver.resolve(uri);
const notebook = resourceRef.object.notebook;
const originalUri = ChatEditingNotebookFileSystemProvider.getSnapshotFileURI(telemetryInfo.requestId, notebook.uri.path);
protected override _doAccept(tx: ITransaction | undefined): Promise<void> {
throw new Error('Method not implemented.');
const [options, buffer] = await Promise.all([
notebookService.withNotebookDataProvider(resourceRef.object.notebook.notebookType),
notebookService.createNotebookTextDocumentSnapshot(notebook.uri, SnapshotContext.Backup, CancellationToken.None).then(s => streamToBuffer(s))
]);
const originalDisposables = new DisposableStore();
originalDisposables.add(ChatEditingNotebookFileSystemProvider.registerFile(originalUri, buffer));
const originalRef = await resolver.resolve(originalUri, notebook.viewType);
originalDisposables.add(originalRef);
if (initialContent) {
restoreSnapshot(originalRef.object.notebook, initialContent);
}
initialContent = initialContent || createSnapshot(originalRef.object.notebook, options.serializer.options, configurationServie);
return instantiationService.createInstance(ChatEditingModifiedNotebookEntry, resourceRef, originalRef, _multiDiffEntryDelegate, options.serializer.options, telemetryInfo, chatKind, initialContent);
});
}
protected override _doReject(tx: ITransaction | undefined): Promise<void> {
throw new Error('Method not implemented.');
constructor(
modifiedResourceRef: IReference<IResolvedNotebookEditorModel>,
originalResourceRef: IReference<IResolvedNotebookEditorModel>,
private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void },
private readonly transientOptions: TransientOptions | undefined,
telemetryInfo: IModifiedEntryTelemetryInfo,
kind: ChatEditKind,
initialContent: string,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFilesConfigurationService fileConfigService: IFilesConfigurationService,
@IChatService chatService: IChatService,
@IFileService fileService: IFileService,
@IInstantiationService instantiationService: IInstantiationService
) {
super(modifiedResourceRef.object.notebook.uri, telemetryInfo, kind, configurationService, fileConfigService, chatService, fileService, instantiationService);
this._register(modifiedResourceRef);
this._register(originalResourceRef);
this.modifiedModel = modifiedResourceRef.object.notebook;
this.originalModel = originalResourceRef.object.notebook;
this.originalURI = this.originalModel.uri;
this.initialContent = initialContent;
}
protected override async _doAccept(tx: ITransaction | undefined): Promise<void> {
const outputSizeLimit = this.configurationService.getValue<number>(NotebookSetting.outputBackupSizeLimit) * 1024;
const snapshot = this.modifiedModel.createSnapshot({ context: SnapshotContext.Backup, outputSizeLimit, transientOptions: this.transientOptions });
this.originalModel.restoreSnapshot(snapshot, this.transientOptions);
this._changesCount.set(0, tx);
// this._diffInfo.set(nullDocumentDiff, tx);
// this._edit = OffsetEdit.empty;
await this._collapse(tx);
}
protected override async _doReject(tx: ITransaction | undefined): Promise<void> {
if (this.createdInRequestId === this._telemetryInfo.requestId) {
// await this.docFileEditorModel.revert({ soft: true });
await this._fileService.del(this.modifiedURI);
this._onDidDelete.fire();
} else {
const outputSizeLimit = this.configurationService.getValue<number>(NotebookSetting.outputBackupSizeLimit) * 1024;
const snapshot = this.originalModel.createSnapshot({ context: SnapshotContext.Backup, outputSizeLimit, transientOptions: this.transientOptions });
this.modifiedModel.restoreSnapshot(snapshot, this.transientOptions);
// if (this._allEditsAreFromUs) {
// // // save the file after discarding so that the dirty indicator goes away
// // // and so that an intermediate saved state gets reverted
// // await this.docFileEditorModel.save({ reason: SaveReason.EXPLICIT, skipSaveParticipants: true });
// }
await this._collapse(tx);
}
}
private async _collapse(transaction: ITransaction | undefined): Promise<void> {
this._multiDiffEntryDelegate.collapse(transaction);
}
protected override _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration {
@@ -48,18 +141,78 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie
}
override createSnapshot(requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry {
throw new Error('Method not implemented.');
return {
resource: this.modifiedURI,
languageId: 'notebook',
snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(this._telemetryInfo.sessionId, requestId, undoStop, this.modifiedURI.path),
original: this.initialContent,
current: createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService),
originalToCurrentEdit: OffsetEdit.empty,
state: this.state.get(),
telemetryInfo: this.telemetryInfo,
};
}
override equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean {
throw new Error('Method not implemented.');
return !!snapshot &&
this.modifiedURI.toString() === snapshot.resource.toString() &&
this.state.get() === snapshot.state &&
createSnapshot(this.originalModel, this.transientOptions, this.configurationService) === snapshot.original &&
createSnapshot(this.modifiedModel, this.transientOptions, this.configurationService) === snapshot.current;
}
override restoreFromSnapshot(snapshot: ISnapshotEntry): void {
throw new Error('Method not implemented.');
this._stateObs.set(snapshot.state, undefined);
restoreSnapshot(this.originalModel, snapshot.original);
restoreSnapshot(this.modifiedModel, snapshot.current);
// this._edit = snapshot.originalToCurrentEdit;
// this._updateDiffInfoSeq();
restoreSnapshot(this.modifiedModel, snapshot.current);
}
override resetToInitialContent(): void {
throw new Error('Method not implemented.');
restoreSnapshot(this.modifiedModel, this.initialContent);
}
}
const BufferMarker = 'ArrayBuffer-4f56482b-5a03-49ba-8356-210d3b0c1c3d';
function createSnapshot(notebook: NotebookTextModel, transientOptions: TransientOptions | undefined, configurationService: IConfigurationService): string {
const outputSizeLimit = configurationService.getValue<number>(NotebookSetting.outputBackupSizeLimit) * 1024;
return serializeSnapshot(notebook.createSnapshot({ context: SnapshotContext.Backup, outputSizeLimit, transientOptions }), transientOptions);
}
function restoreSnapshot(notebook: NotebookTextModel, snapshot: string): void {
const { transientOptions, data } = deserializeSnapshot(snapshot);
notebook.restoreSnapshot(data, transientOptions);
}
function serializeSnapshot(data: NotebookData, transientOptions: TransientOptions | undefined): string {
return JSON.stringify([
JSON.stringify(transientOptions)
, JSON.stringify(data, (_key, value) => {
if (value instanceof VSBuffer) {
return {
type: BufferMarker,
data: encodeBase64(value)
};
}
return value;
})
]);
}
function deserializeSnapshot(snapshot: string): { transientOptions: TransientOptions | undefined; data: NotebookData } {
const [transientOptionsStr, dataStr] = JSON.parse(snapshot);
const transientOptions = transientOptionsStr ? JSON.parse(transientOptionsStr) as TransientOptions : undefined;
const data: NotebookData = JSON.parse(dataStr, (_key, value) => {
if (value && value.type === BufferMarker) {
return decodeBase64(value.data);
}
return value;
});
return { transientOptions, data };
}
@@ -369,24 +369,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
});
}
async getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> {
const currentSession = this.getEditingSession(chatSessionId);
if (!currentSession) {
return undefined;
}
const userAddedWorkingSetEntries: URI[] = [];
for (const [uri, metadata] of currentSession.workingSet) {
// Don't incorporate suggested files into the related files request
// but do consider transient entries like open editors
if (metadata.state !== WorkingSetEntryState.Suggested) {
userAddedWorkingSetEntries.push(uri);
}
}
async getRelatedFiles(chatSessionId: string, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> {
const providers = Array.from(this._chatRelatedFilesProviders.values());
const result = await Promise.all(providers.map(async provider => {
try {
const relatedFiles = await provider.provideRelatedFiles({ prompt, files: userAddedWorkingSetEntries }, token);
const relatedFiles = await provider.provideRelatedFiles({ prompt, files }, token);
if (relatedFiles?.length) {
return { group: provider.description, files: relatedFiles };
}
@@ -17,7 +17,6 @@ import { asyncTransaction, autorun, derived, derivedOpts, derivedWithStore, IObs
import { autorunDelta, autorunIterableDelta } from '../../../../../base/common/observableInternal/autorun.js';
import { isEqual, joinPath } from '../../../../../base/common/resources.js';
import { URI } from '../../../../../base/common/uri.js';
import { isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js';
import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js';
import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js';
import { TextEdit } from '../../../../../editor/common/languages.js';
@@ -35,31 +34,27 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com
import { ILogService } from '../../../../../platform/log/common/log.js';
import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { IEditorCloseEvent, SaveReason } from '../../../../common/editor.js';
import { SaveReason } from '../../../../common/editor.js';
import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js';
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { ITextFileService } from '../../../../services/textfile/common/textfiles.js';
import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js';
import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';
import { isNotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js';
import { INotebookService } from '../../../notebook/common/notebookService.js';
import { ChatEditingSessionChangeType, ChatEditingSessionState, chatEditingSnapshotScheme, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedFileEntry, IStreamingEdits, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedFileEntry, IStreamingEdits, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { IChatRequestDisablement, IChatResponseModel } from '../../common/chatModel.js';
import { IChatService } from '../../common/chatService.js';
import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js';
import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js';
import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';
import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js';
const STORAGE_CONTENTS_FOLDER = 'contents';
const STORAGE_STATE_FILE = 'state.json';
const POST_EDIT_STOP_ID = 'd19944f6-f46c-4e17-911b-79a8e843c7c0'; // randomly generated
const untrackedSchemes: readonly string[] = [
chatEditingSnapshotScheme,
];
class ThrottledSequencer extends Sequencer {
private _size = 0;
@@ -234,12 +229,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
});
}
// Add the currently active editors to the working set
this._trackCurrentEditorsInWorkingSet();
this._triggerSaveParticipantsOnAccept();
this._register(this._editorService.onDidVisibleEditorsChange(() => {
this._trackCurrentEditorsInWorkingSet();
}));
this._register(autorun(reader => {
const entries = this.entries.read(reader);
entries.forEach(entry => {
@@ -250,6 +240,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
private _getEntry(uri: URI): AbstractChatEditingModifiedFileEntry | undefined {
uri = CellUri.parse(uri)?.notebook ?? uri;
return this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri));
}
@@ -258,6 +249,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
public readEntry(uri: URI, reader: IReader | undefined): IModifiedFileEntry | undefined {
uri = CellUri.parse(uri)?.notebook ?? uri;
return this._entriesObs.read(reader).find(e => isEqual(e.modifiedURI, uri));
}
@@ -306,61 +298,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
));
}
private _trackCurrentEditorsInWorkingSet(e?: IEditorCloseEvent) {
const existingTransientEntries = new ResourceSet();
for (const file of this._workingSet.keys()) {
if (this._workingSet.get(file)?.state === WorkingSetEntryState.Transient) {
existingTransientEntries.add(file);
}
}
const activeEditors = new ResourceSet();
this._editorGroupsService.groups.forEach((group) => {
if (!group.activeEditorPane) {
return;
}
let uri;
if (isNotebookEditorInput(group.activeEditorPane.input)) {
uri = group.activeEditorPane.input.resource;
} else {
let activeEditorControl = group.activeEditorPane.getControl();
if (isDiffEditor(activeEditorControl)) {
activeEditorControl = activeEditorControl.getOriginalEditor().hasTextFocus() ? activeEditorControl.getOriginalEditor() : activeEditorControl.getModifiedEditor();
}
if ((isCodeEditor(activeEditorControl)) && activeEditorControl.hasModel()) {
uri = activeEditorControl.getModel().uri;
}
}
if (!uri) {
return;
}
if (untrackedSchemes.includes(uri.scheme)) {
return;
}
if (existingTransientEntries.has(uri)) {
existingTransientEntries.delete(uri);
} else if ((!this._workingSet.has(uri) || this._workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) && !this._removedTransientEntries.has(uri)) {
// Don't add as a transient entry if it's already a confirmed part of the working set
// or if the user has intentionally removed it from the working set
activeEditors.add(uri);
}
});
let didChange = false;
for (const entry of existingTransientEntries) {
didChange = this._workingSet.delete(entry) || didChange;
}
for (const entry of activeEditors) {
this._workingSet.set(entry, { state: WorkingSetEntryState.Transient, description: localize('chatEditing.transient', "Open Editor") });
didChange = true;
}
if (didChange) {
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
}
}
private _findSnapshot(requestId: string): IChatEditingSessionSnapshot | undefined {
return this._linearHistory.get().find(s => s.requestId === requestId);
}
@@ -610,7 +547,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
const state = this._workingSet.get(uri);
if (state !== undefined) {
didRemoveUris = this._workingSet.delete(uri) || didRemoveUris;
if (reason === WorkingSetEntryRemovalReason.User && (state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested)) {
if (reason === WorkingSetEntryRemovalReason.User && state.state === WorkingSetEntryState.Suggested) {
this._removedTransientEntries.add(uri);
}
}
@@ -626,7 +563,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
markIsReadonly(resource: URI, isReadonly?: boolean): void {
const entry = this._workingSet.get(resource);
if (entry) {
if (entry.state === WorkingSetEntryState.Transient || entry.state === WorkingSetEntryState.Suggested) {
if (entry.state === WorkingSetEntryState.Suggested) {
entry.state = WorkingSetEntryState.Attached;
}
entry.isMarkedReadonly = isReadonly ?? !entry.isMarkedReadonly;
@@ -850,7 +787,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
this._workingSet.set(resource, { description, state: WorkingSetEntryState.Suggested });
this._trackUntitledWorkingSetEntry(resource);
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
} else if (state === undefined || state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested) {
} else if (state === undefined || state.state === WorkingSetEntryState.Suggested) {
this._workingSet.set(resource, { description, state: WorkingSetEntryState.Attached });
this._trackUntitledWorkingSetEntry(resource);
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
@@ -1086,12 +1023,16 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise<AbstractChatEditingModifiedFileEntry> {
const multiDiffEntryDelegate = { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) };
const chatKind = mustExist ? ChatEditKind.Created : ChatEditKind.Modified;
try {
const ref = await this._textModelService.createModelReference(resource);
const ctor = this._notebookService.hasSupportedNotebooks(resource)
? ChatEditingModifiedDocumentEntry // TODO@DonJayamanne use NotebookEntry once ready
: ChatEditingModifiedDocumentEntry;
return this._instantiationService.createInstance(ctor, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, telemetryInfo, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, initialContent);
const notebookUri = CellUri.parse(resource)?.notebook || resource;
if (this._notebookService.hasSupportedNotebooks(notebookUri)) {
return ChatEditingModifiedNotebookEntry.create(notebookUri, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent, this._instantiationService);
} else {
const ref = await this._textModelService.createModelReference(resource);
return this._instantiationService.createInstance(ChatEditingModifiedDocumentEntry, ref, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent);
}
} catch (err) {
if (mustExist) {
throw err;
@@ -68,7 +68,6 @@ export class ChatEditor extends EditorPane {
undefined,
{
supportsFileReferences: true,
enableImplicitContext: true
},
{
listForeground: editorForeground,
@@ -17,9 +17,7 @@ import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.j
import { Location, SymbolKinds } from '../../../../editor/common/languages.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { DefinitionAction } from '../../../../editor/contrib/gotoSymbol/browser/goToCommands.js';
import * as nls from '../../../../nls.js';
import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
@@ -36,6 +34,7 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ILabelService } from '../../../../platform/label/common/label.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js';
import { fillEditorsDragData } from '../../../browser/dnd.js';
import { ResourceContextKey } from '../../../common/contextkeys.js';
import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
@@ -74,12 +73,11 @@ export class InlineAnchorWidget extends Disposable {
@IHoverService hoverService: IHoverService,
@IInstantiationService instantiationService: IInstantiationService,
@ILabelService labelService: ILabelService,
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
@ILanguageService languageService: ILanguageService,
@IMenuService menuService: IMenuService,
@IModelService modelService: IModelService,
@ITextModelService textModelService: ITextModelService,
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
) {
super();
@@ -133,7 +131,15 @@ export class InlineAnchorWidget extends Disposable {
label;
const fileKind = location.uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE;
iconClasses = getIconClasses(modelService, languageService, location.uri, fileKind);
const recomputeIconClasses = () => getIconClasses(modelService, languageService, location.uri, fileKind, fileKind === FileKind.FOLDER && !themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined);
iconClasses = recomputeIconClasses();
this._register(themeService.onDidFileIconThemeChange(() => {
iconEl.classList.remove(...iconClasses);
iconClasses = recomputeIconClasses();
iconEl.classList.add(...iconClasses);
}));
const isFolderContext = ExplorerFolderContext.bindTo(contextKeyService);
fileService.stat(location.uri)
@@ -15,9 +15,7 @@ import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/
import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';
import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js';
import { IAction, Separator, toAction } from '../../../../base/common/actions.js';
import { coalesce } from '../../../../base/common/arrays.js';
import { Promises } from '../../../../base/common/async.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../base/common/event.js';
@@ -35,7 +33,6 @@ import { EditorOptions } from '../../../../editor/common/config/editorOptions.js
import { IDimension } from '../../../../editor/common/core/dimension.js';
import { IPosition } from '../../../../editor/common/core/position.js';
import { IRange, Range } from '../../../../editor/common/core/range.js';
import { isLocation } from '../../../../editor/common/languages.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js';
@@ -81,9 +78,8 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd
import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js';
import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { ChatEditingSessionState, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js';
import { IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js';
import { IChatRequestVariableEntry, isPasteVariableEntry } from '../common/chatModel.js';
import { ChatRequestDynamicVariablePart } from '../common/chatParserTypes.js';
import { IChatFollowup } from '../common/chatService.js';
import { IChatVariablesService } from '../common/chatVariables.js';
import { IChatResponseViewModel } from '../common/chatViewModel.js';
@@ -93,7 +89,7 @@ import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSeco
import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js';
import { InstructionAttachmentsWidget } from './attachments/instructionsAttachment/instructionAttachments.js';
import { IChatWidget } from './chat.js';
import { ChatAttachmentModel, EditsAttachmentModel } from './chatAttachmentModel.js';
import { ChatAttachmentModel } from './chatAttachmentModel.js';
import { toChatVariable } from './chatAttachmentModel/chatInstructionAttachmentsModel.js';
import { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js';
import { IDisposableReference } from './chatContentParts/chatCollections.js';
@@ -124,7 +120,6 @@ interface IChatInputPartOptions {
telemetrySource?: string;
};
editorOverflowWidgetsDomNode?: HTMLElement;
enableImplicitContext?: boolean;
renderWorkingSet?: boolean;
}
@@ -243,6 +238,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private attachedContextContainer!: HTMLElement;
private readonly attachedContextDisposables = this._register(new MutableDisposable<DisposableStore>());
private relatedFilesContainer!: HTMLElement;
private chatEditingSessionWidgetContainer!: HTMLElement;
private _inputPartHeight: number = 0;
@@ -299,7 +296,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private readonly _chatEditsActionsDisposables = this._register(new DisposableStore());
private readonly _chatEditsDisposables = this._register(new DisposableStore());
private _chatEditsProgress: ProgressBar | undefined;
private _chatEditsListPool: CollapsibleListPool;
private _chatEditList: IDisposableReference<WorkbenchList<IChatCollapsibleListItem>> | undefined;
get selectedElements(): URI[] {
@@ -322,10 +318,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
public get attemptedWorkingSetEntriesCount() {
return this._attemptedWorkingSetEntriesCount;
}
private _combinedChatEditWorkingSetEntries: IWorkingSetEntry[] = [];
public get chatEditWorkingSetFiles() {
return this._combinedChatEditWorkingSetEntries;
}
private readonly getInputState: () => IChatInputState;
@@ -365,11 +357,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
) {
super();
this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel));
if (this.location === ChatAgentLocation.EditingSession) {
this._attachmentModel = this._register(this.instantiationService.createInstance(EditsAttachmentModel));
this.dnd = this._register(this.instantiationService.createInstance(EditsDragAndDrop, this.attachmentModel, styles));
} else {
this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel));
this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, this.attachmentModel, styles));
}
@@ -684,6 +675,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
]),
]),
dom.h('.chat-attached-context@attachedContextContainer'),
dom.h('.chat-related-files@relatedFilesContainer'),
dom.h('.interactive-input-followups@followupsContainer'),
])
]);
@@ -693,8 +685,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'),
dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [
dom.h('.chat-input-container@inputContainer', [
dom.h('.chat-editor-container@editorContainer'),
dom.h('.chat-attached-context@attachedContextContainer'),
dom.h('.chat-related-files@relatedFilesContainer'),
dom.h('.chat-editor-container@editorContainer'),
dom.h('.chat-input-toolbars@inputToolbars'),
]),
]),
@@ -708,13 +701,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars
const editorContainer = elements.editorContainer;
this.attachedContextContainer = elements.attachedContextContainer;
this.relatedFilesContainer = elements.relatedFilesContainer;
const toolbarsContainer = elements.inputToolbars;
this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer;
this.renderAttachedContext();
if (this.options.enableImplicitContext) {
this._implicitContext = this._register(new ChatImplicitContext());
this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange()));
}
this.renderAttachedContext(widget);
this._implicitContext = this._register(new ChatImplicitContext());
this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange()));
this._register(this._attachmentModel.onDidChangeContext(() => this._handleAttachedContextChange()));
this.renderChatEditingSessionState(null, widget);
@@ -830,7 +822,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
onDidChangeModel: this._onDidChangeCurrentLanguageModel.event,
setModel: (model: ILanguageModelChatMetadataAndIdentifier) => {
this.setCurrentLanguageModelByUser(model);
this.renderAttachedContext();
this.renderAttachedContext(widget);
},
getModels: () => this.getModels()
};
@@ -907,11 +899,29 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
onDidChangeCursorPosition();
this._register(this.themeService.onDidFileIconThemeChange(() => {
this.renderAttachedContext();
this.renderAttachedContext(widget);
}));
}
private async renderAttachedContext() {
private renderAddFilesHint(container: HTMLElement, hoverDelegate: IHoverDelegate, store: DisposableStore, chatWidget: IChatWidget) {
const button = store.add(new Button(container, {
supportIcons: true,
secondary: true,
hoverDelegate
}));
button.element.classList.add('chat-attached-context-attachment', 'chat-add-files');
button.label = localize('chatAddFiles', '{0} Add Files...', '$(attach)');
const commandId = 'workbench.action.chat.editing.attachFiles';
const kb = this.keybindingService.lookupKeybinding(commandId)?.getLabel();
const title = localize('attachFiles.label', 'Attach files to your request{0}', kb ? ` (${kb})` : '');
button.setTitle(title);
store.add(button.onDidClick(() => {
this.commandService.executeCommand(commandId, { widget: chatWidget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') });
}));
dom.append(container, button.element);
}
private async renderAttachedContext(widget?: IChatWidget) {
const container = this.attachedContextContainer;
const oldHeight = container.offsetHeight;
const store = new DisposableStore();
@@ -919,11 +929,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
dom.clearNode(container);
const hoverDelegate = store.add(createInstantHoverDelegate());
const attachments = this.location === ChatAgentLocation.EditingSession
// Render as attachments anything that isn't a file, but still render specific ranges in a file
? [...this.attachmentModel.attachments.entries()].filter(([_, attachment]) => !attachment.isFile || attachment.isFile && typeof attachment.value === 'object' && !!attachment.value && 'range' in attachment.value)
: [...this.attachmentModel.attachments.entries()];
dom.setVisibility(Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty, this.attachedContextContainer);
if (widget && this.options.renderWorkingSet) {
this.renderAddFilesHint(container, hoverDelegate, store, widget);
}
const attachments = [...this.attachmentModel.attachments.entries()];
dom.setVisibility(Boolean(widget && this.options.renderWorkingSet) || Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty, this.attachedContextContainer);
if (!attachments.length) {
this._indexOfLastAttachedContextDeletedWithKeyboard = -1;
}
@@ -1193,22 +1203,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null, chatWidget?: IChatWidget) {
dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer);
if (!chatEditingSession || !this.options.renderWorkingSet) {
dom.clearNode(this.chatEditingSessionWidgetContainer);
this._chatEditsDisposables.clear();
this._chatEditList = undefined;
this._combinedChatEditWorkingSetEntries = [];
this._chatEditsProgress?.dispose();
return;
await this.renderAttachedContext(chatWidget);
if (chatEditingSession) {
this.renderChatRelatedFiles(chatEditingSession, this.relatedFilesContainer);
}
const currentChatEditingState = chatEditingSession.state.get();
if (this._chatEditList && !chatWidget?.viewModel?.requestInProgress && (currentChatEditingState === ChatEditingSessionState.Idle || currentChatEditingState === ChatEditingSessionState.Initial)) {
this._chatEditsProgress?.stop();
}
// Summary of number of files changed
const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons'));
const seenEntries = new ResourceSet();
const entries: IChatCollapsibleListItem[] = chatEditingSession?.entries.get().map((entry) => {
seenEntries.add(entry.modifiedURI);
@@ -1218,16 +1217,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
kind: 'reference',
};
}) ?? [];
for (const attachment of (this.attachmentModel as EditsAttachmentModel).fileAttachments) {
if (URI.isUri(attachment.value) && !seenEntries.has(attachment.value)) {
entries.unshift({
reference: attachment.value,
state: WorkingSetEntryState.Attached,
kind: 'reference',
});
seenEntries.add(attachment.value);
}
if (!chatEditingSession || !this.options.renderWorkingSet || !entries.length) {
dom.clearNode(this.chatEditingSessionWidgetContainer);
this._chatEditsDisposables.clear();
this._chatEditList = undefined;
return;
}
// Summary of number of files changed
const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons'));
for (const [file, metadata] of chatEditingSession.workingSet.entries()) {
if (!seenEntries.has(file) && metadata.state !== WorkingSetEntryState.Suggested) {
entries.unshift({
@@ -1240,16 +1239,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
seenEntries.add(file);
}
}
// Factor file variables that are part of the user query into the working set
for (const part of chatWidget?.parsedInput.parts ?? []) {
if (part instanceof ChatRequestDynamicVariablePart && part.isFile && (URI.isUri(part.data) && !seenEntries.has(part.data) || isLocation(part.data) && !seenEntries.has(part.data.uri))) {
entries.unshift({
reference: part.data,
state: WorkingSetEntryState.Attached,
kind: 'reference',
});
}
}
entries.sort((a, b) => {
if (a.kind === 'reference' && b.kind === 'reference') {
@@ -1260,18 +1249,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
}
return 0;
});
const overviewRegion = innerContainer.querySelector('.chat-editing-session-overview') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-overview'));
const overviewTitle = overviewRegion.querySelector('.working-set-title') as HTMLElement ?? dom.append(overviewRegion, $('.working-set-title'));
const overviewWorkingSet = overviewTitle.querySelector('span') ?? dom.append(overviewTitle, $('span'));
const overviewFileCount = overviewTitle.querySelector('span.working-set-count') ?? dom.append(overviewTitle, $('span.working-set-count'));
overviewWorkingSet.textContent = localize('chatEditingSession.workingSet', 'Working Set');
let suggestedFilesInWorkingSetCount = 0;
overviewFileCount.textContent = '';
if (entries.length === 1) {
overviewFileCount.textContent = ' ' + localize('chatEditingSession.oneFile', '(1 file)');
overviewFileCount.textContent = localize('chatEditingSession.oneFile.1', '1 file changed');
suggestedFilesInWorkingSetCount = entries[0].kind === 'reference' && entries[0].state === WorkingSetEntryState.Suggested ? 1 : 0;
} else {
suggestedFilesInWorkingSetCount = entries.filter(e => e.kind === 'reference' && e.state === WorkingSetEntryState.Suggested).length;
@@ -1279,10 +1265,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
if (entries.length > 1) {
const fileCount = entries.length - suggestedFilesInWorkingSetCount;
overviewFileCount.textContent = ' ' + (fileCount === 1 ? localize('chatEditingSession.oneFile', '(1 file)') : localize('chatEditingSession.manyFiles', '({0} files)', fileCount));
overviewFileCount.textContent = (fileCount === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', fileCount));
}
overviewTitle.ariaLabel = overviewWorkingSet.textContent + overviewFileCount.textContent;
overviewTitle.ariaLabel = overviewFileCount.textContent;
overviewTitle.tabIndex = 0;
// Clear out the previous actions (if any)
@@ -1308,11 +1294,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
return;
}
if (currentChatEditingState === ChatEditingSessionState.StreamingEdits || chatWidget?.viewModel?.requestInProgress) {
// this._chatEditsProgress ??= new ProgressBar(innerContainer);
this._chatEditsProgress?.infinite().show(500);
}
// Working set
const workingSetContainer = innerContainer.querySelector('.chat-editing-session-list') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-list'));
if (!this._chatEditList) {
@@ -1354,53 +1335,42 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
list.layout(height);
list.getHTMLElement().style.height = `${height}px`;
list.splice(0, list.length, entries);
this._combinedChatEditWorkingSetEntries = coalesce(entries.map((e) => e.kind === 'reference' && URI.isUri(e.reference) ? ({ uri: e.reference, isMarkedReadonly: e.isMarkedReadonly }) : undefined));
const addFilesElement = innerContainer.querySelector('.chat-editing-session-toolbar-actions') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-toolbar-actions'));
this._onDidChangeHeight.fire();
}
async renderChatRelatedFiles(chatEditingSession: IChatEditingSession, anchor: HTMLElement) {
dom.clearNode(anchor);
const hoverDelegate = getDefaultHoverDelegate('element');
const button = this._chatEditsActionsDisposables.add(new Button(addFilesElement, {
supportIcons: true,
secondary: true,
hoverDelegate
}));
button.label = localize('chatAddFiles', '{0} Add Files...', '$(add)');
button.setTitle(button.enabled ? localize('addFiles.label', 'Add files to your working set') : localize('chatEditingSession.fileLimitReached', 'You have reached the maximum number of files that can be added to the working set.'));
this._chatEditsActionsDisposables.add(button.onDidClick(() => {
this.commandService.executeCommand('workbench.action.chat.editing.attachFiles', { widget: chatWidget });
}));
dom.append(addFilesElement, button.element);
// RELATED files (after Add Files...)
for (const [uri, metadata] of chatEditingSession.workingSet) {
if (metadata.state !== WorkingSetEntryState.Suggested) {
continue;
}
const addBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, {
const uriLabel = this._chatEditsActionsDisposables.add(new Button(anchor, {
supportIcons: true,
secondary: true,
hoverDelegate
}));
addBtn.label = this.labelService.getUriBasenameLabel(uri);
addBtn.element.classList.add('monaco-icon-label', ...getIconClasses(this.modelService, this.languageService, uri, FileKind.FILE));
addBtn.setTitle(localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), metadata.description ?? ''));
uriLabel.label = this.labelService.getUriBasenameLabel(uri);
uriLabel.element.classList.add('monaco-icon-label', ...getIconClasses(this.modelService, this.languageService, uri, FileKind.FILE));
uriLabel.element.title = localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), metadata.description ?? '');
this._chatEditsActionsDisposables.add(addBtn.onDidClick(() => {
this._chatEditsActionsDisposables.add(uriLabel.onDidClick(() => {
group.remove(); // REMOVE asap
chatEditingSession.addFileToWorkingSet(uri);
this._attachmentModel.addFile(uri);
chatEditingSession.remove(WorkingSetEntryRemovalReason.User, uri);
}));
const rmBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, {
const addButton = this._chatEditsActionsDisposables.add(new Button(anchor, {
supportIcons: false,
secondary: true,
hoverDelegate,
ariaLabel: localize('chatEditingSession.removeSuggestion', 'Remove suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })),
ariaLabel: localize('chatEditingSession.addSuggestion', 'Add suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })),
}));
rmBtn.icon = Codicon.close;
rmBtn.setTitle(localize('chatEditingSession.removeSuggested', 'Remove suggestion'));
this._chatEditsActionsDisposables.add(rmBtn.onDidClick(() => {
addButton.icon = Codicon.add;
addButton.setTitle(localize('chatEditingSession.addSuggested', 'Add suggestion'));
this._chatEditsActionsDisposables.add(addButton.onDidClick(() => {
group.remove(); // REMOVE asap
this._attachmentModel.addFile(uri);
chatEditingSession.remove(WorkingSetEntryRemovalReason.User, uri);
}));
@@ -1408,17 +1378,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
sep.classList.add('separator');
const group = document.createElement('span');
group.classList.add('monaco-button-dropdown', 'sidebyside-button');
group.appendChild(addBtn.element);
group.classList.add('monaco-button-dropdown', 'sidebyside-button', 'show-file-icons');
group.appendChild(uriLabel.element);
group.appendChild(sep);
group.appendChild(rmBtn.element);
dom.append(addFilesElement, group);
group.appendChild(addButton.element);
dom.append(anchor, group);
this._chatEditsActionsDisposables.add(toDisposable(() => {
group.remove();
}));
}
this._onDidChangeHeight.fire();
}
async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise<void> {
@@ -1484,7 +1454,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight),
inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32,
inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 28,
attachmentsHeight: this.attachedContextContainer.offsetHeight,
attachmentsHeight: this.attachedContextContainer.offsetHeight + this.relatedFilesContainer.offsetHeight,
editorBorder: 2,
inputPartHorizontalPaddingInside: 12,
toolbarsWidth: this.options.renderStyle === 'compact' ? executeToolbarWidth + executeToolbarPadding + inputToolbarWidth + inputToolbarPadding : 0,
@@ -578,7 +578,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
if (!isFiltered) {
if (isRequestVM(element) && element.variables.length) {
const newPart = this.renderAttachments(element.variables, element.contentReferences, element.workingSet, templateData);
const newPart = this.renderAttachments(element.variables, element.contentReferences, templateData);
if (newPart) {
if (newPart.domNode) {
// p has a :last-child rule for margin
@@ -951,8 +951,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return part;
}
private renderAttachments(variables: IChatRequestVariableEntry[], contentReferences: ReadonlyArray<IChatContentReference> | undefined, workingSet: ReadonlyArray<URI> | undefined, templateData: IChatListItemTemplate) {
return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, workingSet, undefined);
private renderAttachments(variables: IChatRequestVariableEntry[], contentReferences: ReadonlyArray<IChatContentReference> | undefined, templateData: IChatListItemTemplate) {
return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined);
}
private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart {
@@ -1014,6 +1014,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
disposeElement(node: ITreeNode<ChatTreeItem, FuzzyScore>, index: number, templateData: IChatListItemTemplate): void {
this.traceLayout('disposeElement', `Disposing element, index=${index}`);
templateData.elementDisposables.clear();
// Don't retain the toolbar context which includes chat viewmodels
if (templateData.titleToolbar) {
templateData.titleToolbar.context = undefined;
}
templateData.footerToolbar.context = undefined;
}
disposeTemplate(templateData: IChatListItemTemplate): void {
@@ -221,7 +221,7 @@ class QuickChat extends Disposable {
ChatWidget,
ChatAgentLocation.Panel,
{ isQuickChat: true },
{ autoScroll: true, renderInputOnTop: true, renderStyle: 'compact', menus: { inputSideToolbar: MenuId.ChatInputSide }, enableImplicitContext: true },
{ autoScroll: true, renderInputOnTop: true, renderStyle: 'compact', menus: { inputSideToolbar: MenuId.ChatInputSide } },
{
listForeground: quickInputForeground,
listBackground: quickInputBackground,
@@ -185,7 +185,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
referencesExpandedWhenEmptyResponse: this.chatOptions.location !== ChatAgentLocation.EditingSession,
progressMessageAtBottomOfResponse: this.chatOptions.location === ChatAgentLocation.EditingSession,
},
enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel,
editorOverflowWidgetsDomNode: editorOverflowNode,
enableWorkingSet: this.chatOptions.location === ChatAgentLocation.EditingSession ? 'explicit' : undefined
},
@@ -830,7 +830,6 @@ export class ChatWidget extends Disposable implements IChatWidget {
renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle,
menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus },
editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode,
enableImplicitContext: this.viewOptions.enableImplicitContext,
renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit'
},
this.styles,
@@ -1100,22 +1099,20 @@ export class ChatWidget extends Disposable implements IChatWidget {
}
let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId);
let workingSet: URI[] | undefined;
if (this.viewOptions.enableWorkingSet !== undefined) {
const currentEditingSession = this._editingSession;
const unconfirmedSuggestions = new ResourceSet();
const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set
const editingSessionAttachedContext: IChatRequestVariableEntry[] = [];
const editingSessionAttachedContext: IChatRequestVariableEntry[] = attachedContext;
// Pick up everything that the user sees is part of the working set.
// This should never exceed the maximum file entries limit above.
for (const { uri, isMarkedReadonly } of this.inputPart.chatEditWorkingSetFiles) {
for (const [uri, state] of currentEditingSession.get()?.workingSet || []) {
// Skip over any suggested files that haven't been confirmed yet in the working set
if (currentEditingSession.get()?.workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) {
if (state.state === WorkingSetEntryState.Suggested) {
unconfirmedSuggestions.add(uri);
} else {
uniqueWorkingSetEntries.add(uri);
editingSessionAttachedContext.push(this.attachmentModel.asVariableEntry(uri, undefined, isMarkedReadonly));
editingSessionAttachedContext.unshift(this.attachmentModel.asVariableEntry(uri, undefined, state.isMarkedReadonly));
}
}
@@ -1153,7 +1150,6 @@ export class ChatWidget extends Disposable implements IChatWidget {
}
}
}
workingSet = [...uniqueWorkingSetEntries.values()];
attachedContext = editingSessionAttachedContext;
type ChatEditingWorkingSetClassification = {
@@ -1166,7 +1162,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
originalSize: number;
actualSize: number;
};
this.telemetryService.publicLog2<ChatEditingWorkingSetEvent, ChatEditingWorkingSetClassification>('chatEditing/workingSetSize', { originalSize: this.inputPart.attemptedWorkingSetEntriesCount, actualSize: uniqueWorkingSetEntries.size });
this.telemetryService.publicLog2<ChatEditingWorkingSetEvent, ChatEditingWorkingSetClassification>('chatEditing/workingSetSize', { originalSize: uniqueWorkingSetEntries.size, actualSize: uniqueWorkingSetEntries.size });
currentEditingSession.get()?.remove(WorkingSetEntryRemovalReason.User, ...unconfirmedSuggestions);
}
@@ -1178,7 +1174,6 @@ export class ChatWidget extends Disposable implements IChatWidget {
locationData: this._location.resolveData?.(),
parserContext: { selectedAgent: this._lastSelectedAgent },
attachedContext,
workingSet,
noCommandDetection: options?.noCommandDetection,
hasInstructionAttachments: this.inputPart.hasInstructionAttachments,
});
@@ -4,11 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { coalesce } from '../../../../../base/common/arrays.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { isCancellationError } from '../../../../../base/common/errors.js';
import * as glob from '../../../../../base/common/glob.js';
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
import { ResourceSet } from '../../../../../base/common/map.js';
import { basename, dirname, joinPath, relativePath } from '../../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
import { IRange, Range } from '../../../../../editor/common/core/range.js';
import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js';
@@ -22,20 +26,19 @@ import { FileType, IFileService } from '../../../../../platform/files/common/fil
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js';
import { PromptsConfig } from '../../../../../platform/prompts/common/config.js';
import { IQuickAccessOptions } from '../../../../../platform/quickinput/common/quickAccess.js';
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { getExcludes, ISearchConfiguration, IFileQuery, QueryType, ISearchComplete, ISearchService } from '../../../../services/search/common/search.js';
import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js';
import { ISymbolQuickPickItem } from '../../../search/browser/symbolsQuickAccess.js';
import { IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js';
import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js';
import { PromptFilesConfig } from '../../common/promptSyntax/config.js';
import * as glob from '../../../../../base/common/glob.js';
import { IChatWidget } from '../chat.js';
import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js';
import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { Codicon } from '../../../../../base/common/codicons.js';
export const dynamicVariableDecorationType = 'chat-dynamic-variable';
@@ -139,7 +142,7 @@ export class ChatDynamicVariableModel extends Disposable implements IChatWidgetC
addReference(ref: IDynamicVariable): void {
// use `ChatFileReference` for file references and `IDynamicVariable` for other variables
const promptSnippetsEnabled = PromptFilesConfig.enabled(this.configService);
const promptSnippetsEnabled = PromptsConfig.enabled(this.configService);
const variable = (ref.id === 'vscode.file' && promptSnippetsEnabled)
? this.instantiationService.createInstance(ChatFileReference, ref)
: ref;
@@ -645,3 +648,57 @@ export class AddDynamicVariableAction extends Action2 {
}
}
registerAction2(AddDynamicVariableAction);
export async function createMarkersQuickPick(accessor: ServicesAccessor): Promise<IDiagnosticVariableEntryFilterData | undefined> {
const markers = accessor.get(IMarkerService).read();
if (!markers.length) {
return;
}
const uriIdentityService = accessor.get(IUriIdentityService);
const labelService = accessor.get(ILabelService);
markers.sort((a, b) => uriIdentityService.extUri.compare(a.resource, b.resource) || b.severity - a.severity);
const severities = new Set<MarkerSeverity>();
type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData };
const items: (MarkerPickItem | IQuickPickSeparator)[] = [];
for (const marker of markers) {
if (!uriIdentityService.extUri.isEqual(marker.resource, (items.at(-1) as MarkerPickItem)?.resource)) {
items.push({ type: 'separator', label: labelService.getUriLabel(marker.resource, { relative: true }) });
}
severities.add(marker.severity);
items.push({
type: 'item',
resource: marker.resource,
label: marker.message,
description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn),
entry: { filterUri: marker.resource, filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn } }
});
}
if (items.length === 2) { // single error in a URI
return (items[1] as MarkerPickItem).entry;
}
if (items.length > 2) {
if (severities.has(MarkerSeverity.Error)) {
items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Errors'), entry: { filterSeverity: MarkerSeverity.Error } });
}
if (severities.has(MarkerSeverity.Warning)) {
items.unshift({ type: 'item', label: localize('markers.panel.allWarnings', 'All Warnings'), entry: { filterSeverity: MarkerSeverity.Warning } });
}
if (severities.has(MarkerSeverity.Info)) {
items.unshift({ type: 'item', label: localize('markers.panel.allInfos', 'All Infos'), entry: { filterSeverity: MarkerSeverity.Info } });
}
}
const quickInputService = accessor.get(IQuickInputService);
const quickPick = quickInputService.createQuickPick({ useSeparators: true });
quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...');
quickPick.items = items;
return quickInputService.pick(items, { canPickMany: false }).then(v => v?.entry);
}
@@ -666,7 +666,7 @@ class BuiltinDynamicCompletions extends Disposable {
// RELATED FILES
if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel && this._chatEditingService.getEditingSession(widget.viewModel.sessionId)) {
const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), token), 200)) ?? [];
const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), widget.attachmentModel.fileAttachments, token), 200)) ?? [];
for (const relatedFileGroup of relatedFiles) {
for (const relatedFile of relatedFileGroup.files) {
if (seen.has(relatedFile.uri)) {
@@ -6,12 +6,12 @@
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Event } from '../../../../../base/common/event.js';
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../../base/common/map.js';
import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js';
import { autorun } from '../../../../../base/common/observable.js';
import { URI } from '../../../../../base/common/uri.js';
import { localize } from '../../../../../nls.js';
import { IWorkbenchContribution } from '../../../../common/contributions.js';
import { ChatAgentLocation } from '../../common/chatAgents.js';
import { ChatEditingSessionChangeType, IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { IChatWidget, IChatWidgetService } from '../chat.js';
export class ChatRelatedFilesContribution extends Disposable implements IWorkbenchContribution {
@@ -26,16 +26,15 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben
) {
super();
this._register(
this.chatWidgetService.onDidAddWidget(widget => {
if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel?.sessionId) {
const editingSession = this.chatEditingService.getEditingSession(widget.viewModel.sessionId);
if (editingSession) {
this._handleNewEditingSession(editingSession, widget);
}
this._register(autorun((reader) => {
const sessions = this.chatEditingService.editingSessionsObs.read(reader);
sessions.forEach(session => {
const widget = this.chatWidgetService.getWidgetBySessionId(session.chatSessionId);
if (widget && !this.chatEditingSessionDisposables.has(session.chatSessionId)) {
this._handleNewEditingSession(session, widget);
}
}),
);
});
}));
}
private _updateRelatedFileSuggestions(currentEditingSession: IChatEditingSession, widget: IChatWidget) {
@@ -44,12 +43,12 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben
}
const workingSetEntries = currentEditingSession.entries.get();
if (workingSetEntries.length > 0) {
if (workingSetEntries.length > 0 || widget.attachmentModel.fileAttachments.length === 0) {
// Do this only for the initial working set state
return;
}
this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), CancellationToken.None)
this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None)
.then((files) => {
if (!files?.length || !widget.viewModel?.sessionId) {
return;
@@ -60,6 +59,11 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben
return; // Might have disposed while we were calculating
}
const existingFiles = new ResourceSet(widget.attachmentModel.fileAttachments);
if (!existingFiles.size) {
return;
}
// Pick up to 2 related files
const newSuggestions = new ResourceMap<{ description: string; group: string }>();
for (const group of files) {
@@ -67,7 +71,11 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben
if (newSuggestions.size >= 2) {
break;
}
if (existingFiles.has(file.uri)) {
continue;
}
newSuggestions.set(file.uri, { group: group.group, description: file.description });
existingFiles.add(file.uri);
}
}
@@ -101,10 +109,8 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben
disposableStore.add(onDebouncedType(() => {
this._updateRelatedFileSuggestions(currentEditingSession, widget);
}));
disposableStore.add(currentEditingSession.onDidChange((e) => {
if (e === ChatEditingSessionChangeType.WorkingSet) {
this._updateRelatedFileSuggestions(currentEditingSession, widget);
}
disposableStore.add(widget.attachmentModel.onDidChangeContext(() => {
this._updateRelatedFileSuggestions(currentEditingSession, widget);
}));
disposableStore.add(currentEditingSession.onDidDispose(() => {
disposableStore.dispose();
@@ -685,25 +685,68 @@ have to be updated for changes to the rules above, or to support more deeply nes
justify-content: start;
}
.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:first-child {
.chat-related-files {
padding: 4px 0 0 0;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.chat-related-files .monaco-button-dropdown .monaco-text-button {
font-size: 11px;
justify-content: left;
width: fit-content;
padding: 0px;
border: none;
}
.chat-related-files .monaco-button-dropdown .monaco-text-button span {
font-style: italic;
height: 18px;
opacity: 0.7;
}
.chat-related-files .monaco-button-dropdown {
border-radius: 4px;
height: 18px;
border: 1px solid var(--vscode-input-border);
align-items: center;
overflow: hidden;
padding: 0 0 0 2px;
}
.chat-related-files .monaco-button.codicon.codicon-add {
display: flex;
color: var(--vscode-descriptionForeground);
padding-right: 4px;
padding-left: 4px;
padding-top: 3px;
height: 100%;
}
.interactive-session .chat-related-files .monaco-icon-label::before {
padding: 2px 2px 0 0;
}
.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary:first-child {
margin: 3px 0px 3px 3px;
flex-shrink: 0;
}
.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary.monaco-icon-label::before {
.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary.monaco-icon-label::before {
display: inline-flex;
align-items: center;
}
.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:only-child {
.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary:only-child {
width: 100%;
}
.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary.disabled {
.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary.disabled {
cursor: initial;
}
.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary .codicon {
.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary .codicon {
font-size: 12px;
margin-left: 4px;
}
@@ -1078,6 +1121,13 @@ have to be updated for changes to the rules above, or to support more deeply nes
border-radius: 4px;
height: 18px;
max-width: 100%;
width: fit-content;
}
.interactive-session .chat-attached-context .chat-attached-context-attachment.chat-add-files {
height: 20px;
gap: 0px;
color: var(--vscode-descriptionForeground);
}
.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button {
@@ -1091,6 +1141,8 @@ have to be updated for changes to the rules above, or to support more deeply nes
outline-offset: -4px;
}
.chat-related-files .monaco-button.codicon.codicon-add:hover,
.interactive-session .chat-attached-context .chat-attached-context-attachment.chat-add-files:hover,
.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover {
cursor: pointer;
background: var(--vscode-toolbar-hoverBackground);
@@ -1122,9 +1174,8 @@ have to be updated for changes to the rules above, or to support more deeply nes
}
.interactive-session .chat-attached-context {
padding: 0 0 4px 0;
padding: 5px 0 0 0;
display: flex;
gap: 4px;
flex-wrap: wrap;
cursor: default;
}
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../../../../../nls.js';
import { PROMPT_FILE_EXTENSION } from '../../../../../common/promptSyntax/constants.js';
import { PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js';
import { IQuickInputService } from '../../../../../../../../platform/quickinput/common/quickInput.js';
/**
@@ -9,8 +9,7 @@ import { VSBuffer } from '../../../../../../../../base/common/buffer.js';
import { dirname } from '../../../../../../../../base/common/resources.js';
import { FolderExists, InvalidPromptName, PromptExists } from '../errors.js';
import { IFileService } from '../../../../../../../../platform/files/common/files.js';
import { PROMPT_FILE_EXTENSION } from '../../../../../common/promptSyntax/constants.js';
import { BasePromptParser } from '../../../../../common/promptSyntax/parsers/basePromptParser.js';
import { isPromptFile, PROMPT_FILE_EXTENSION } from '../../../../../../../../platform/prompts/common/constants.js';
/**
* Options for the {@link createPromptFile} utility.
@@ -40,7 +39,7 @@ interface ICreatePromptFileOptions {
* the provided file content.
*
* @throws in the following cases:
* - if the prompt filename does not end with {@link PROMPT_FILE_EXTENSION}
* - if the `fileName` does not end with {@link PROMPT_FILE_EXTENSION}
* - if a folder or file with the same already name exists in the destination folder
*/
export const createPromptFile = async (
@@ -51,7 +50,7 @@ export const createPromptFile = async (
const promptUri = URI.joinPath(folder, fileName);
assert(
BasePromptParser.isPromptSnippet(promptUri),
isPromptFile(promptUri),
new InvalidPromptName(fileName),
);
@@ -8,9 +8,9 @@ import { URI } from '../../../../../../base/common/uri.js';
import { CHAT_CATEGORY } from '../../actions/chatActions.js';
import { IChatWidget, IChatWidgetService } from '../../chat.js';
import { KeyMod, KeyCode } from '../../../../../../base/common/keyCodes.js';
import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js';
import { IEditorService } from '../../../../../services/editor/common/editorService.js';
import { ICommandService } from '../../../../../../platform/commands/common/commands.js';
import { BasePromptParser } from '../../../common/promptSyntax/parsers/basePromptParser.js';
import { appendToCommandPalette } from '../../../../files/browser/fileActions.contribution.js';
import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js';
import { IActiveCodeEditor, isCodeEditor, isDiffEditor } from '../../../../../../editor/browser/editorBrowser.js';
@@ -133,7 +133,7 @@ const getActivePromptUri = (
}
const { uri } = activeEditor.getModel();
if (BasePromptParser.isPromptSnippet(uri)) {
if (isPromptFile(uri)) {
return uri;
}
@@ -41,7 +41,7 @@ export interface IChatEditingService {
hasRelatedFilesProviders(): boolean;
registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable;
getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>;
getRelatedFiles(chatSessionId: string, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>;
//#endregion
}
@@ -88,7 +88,7 @@ export interface IChatEditingSession extends IDisposable {
readonly state: IObservable<ChatEditingSessionState>;
readonly entries: IObservable<readonly IModifiedFileEntry[]>;
readonly workingSet: ResourceMap<WorkingSetDisplayMetadata>;
addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void;
addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Suggested): void;
show(): Promise<void>;
remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void;
markIsReadonly(uri: URI, isReadonly?: boolean): void;
@@ -155,7 +155,7 @@ export const enum WorkingSetEntryState {
Modified,
Accepted,
Rejected,
Transient,
Transient, // TODO@joyceerhl remove this
Attached,
Sent, // TODO@joyceerhl remove this
Suggested,
@@ -21,6 +21,7 @@ import { IRange } from '../../../../editor/common/core/range.js';
import { Location, SymbolKind, TextEdit } from '../../../../editor/common/languages.js';
import { localize } from '../../../../nls.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { MarkerSeverity } from '../../../../platform/markers/common/markers.js';
import { ICellEditOperation } from '../../notebook/common/notebookCommon.js';
import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, IChatWelcomeMessageContent, reviveSerializedAgent } from './chatAgents.js';
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js';
@@ -87,7 +88,39 @@ export interface ILinkVariableEntry extends Omit<IBaseChatRequestVariableEntry,
readonly value: URI;
}
export type IChatRequestVariableEntry = IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | ILinkVariableEntry | IBaseChatRequestVariableEntry;
export interface IDiagnosticVariableEntryFilterData {
readonly filterUri?: URI;
readonly filterSeverity?: MarkerSeverity;
readonly filterRange?: IRange;
}
export namespace IDiagnosticVariableEntryFilterData {
export function id(data: IDiagnosticVariableEntryFilterData) {
return [data.filterUri, data.filterSeverity, data.filterRange?.startLineNumber].join(':');
}
export function label(data: IDiagnosticVariableEntryFilterData) {
let labelStr: string;
if (data.filterSeverity) {
const sev = data.filterRange ? MarkerSeverity.toString(data.filterSeverity) : MarkerSeverity.toStringPlural(data.filterSeverity);
labelStr = data.filterUri
? localize('chat.attachment.problems.severity', "{0} in {1}", sev, basename(data.filterUri))
: localize('chat.attachment.problems.severity2', "All {0}", sev);
} else {
labelStr = data.filterUri
? localize('chat.attachment.problems.severity3', "Problems in {0}", basename(data.filterUri))
: localize('chat.attachment.problems.severity4', "All Problems");
}
return labelStr;
}
}
export interface IDiagnosticVariableEntry extends Omit<IBaseChatRequestVariableEntry, 'kind'>, IDiagnosticVariableEntryFilterData {
readonly kind: 'diagnostic';
}
export type IChatRequestVariableEntry = IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | ILinkVariableEntry | IBaseChatRequestVariableEntry | IDiagnosticVariableEntry;
export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry {
return obj.kind === 'implicit';
@@ -101,6 +134,10 @@ export function isLinkVariableEntry(obj: IChatRequestVariableEntry): obj is ILin
return obj.kind === 'link';
}
export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj is IDiagnosticVariableEntry {
return obj.kind === 'diagnostic';
}
export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry {
const entry = obj as IChatRequestVariableEntry;
return typeof entry === 'object' &&
@@ -125,7 +162,6 @@ export interface IChatRequestModel {
readonly confirmation?: string;
readonly locationData?: IChatLocationData;
readonly attachedContext?: IChatRequestVariableEntry[];
readonly workingSet?: URI[];
readonly isCompleteAddedRequest: boolean;
readonly response?: IChatResponseModel;
shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
@@ -290,10 +326,6 @@ export class ChatRequestModel implements IChatRequestModel {
return this._attachedContext;
}
public get workingSet(): URI[] | undefined {
return this._workingSet;
}
constructor(
private _session: ChatModel,
public readonly message: IParsedChatRequest,
@@ -303,7 +335,6 @@ export class ChatRequestModel implements IChatRequestModel {
private _confirmation?: string,
private _locationData?: IChatLocationData,
private _attachedContext?: IChatRequestVariableEntry[],
private _workingSet?: URI[],
public readonly isCompleteAddedRequest = false,
restoredId?: string,
) {
@@ -1254,7 +1285,7 @@ export class ChatModel extends Disposable implements IChatModel {
// Old messages don't have variableData, or have it in the wrong (non-array) shape
const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData);
const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, raw.workingSet?.map((uri) => URI.revive(uri)), undefined, raw.requestId);
const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, undefined, raw.requestId);
request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;
if (raw.response || raw.result || (raw as any).responseErrorDetails) {
const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format
@@ -1388,7 +1419,7 @@ export class ChatModel extends Disposable implements IChatModel {
}
addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], workingSet?: URI[], isCompleteAddedRequest?: boolean): ChatRequestModel {
const request = new ChatRequestModel(this, message, variableData, Date.now(), attempt, confirmation, locationData, attachments, workingSet, isCompleteAddedRequest);
const request = new ChatRequestModel(this, message, variableData, Date.now(), attempt, confirmation, locationData, attachments, isCompleteAddedRequest);
request.response = new ChatResponseModel([], this, chatAgent, slashCommand, request.id, undefined, undefined, undefined, undefined, undefined, undefined, isCompleteAddedRequest);
this._requests.push(request);
@@ -1548,7 +1579,6 @@ export class ChatModel extends Disposable implements IChatModel {
vote: r.response?.vote,
voteDownReason: r.response?.voteDownReason,
agent: agentJson,
workingSet: r.workingSet,
slashCommand: r.response?.slashCommand,
usedContext: r.response?.usedContext,
contentReferences: r.response?.contentReferences,
@@ -438,7 +438,6 @@ export interface IChatSendRequestOptions {
acceptedConfirmationData?: any[];
rejectedConfirmationData?: any[];
attachedContext?: IChatRequestVariableEntry[];
workingSet?: URI[];
/** The target agent ID can be specified with this property instead of using @ in 'message' */
agentId?: string;
@@ -476,7 +476,6 @@ export class ChatService extends Disposable implements IChatService {
...options,
locationData: request.locationData,
attachedContext: request.attachedContext,
workingSet: request.workingSet,
hasInstructionAttachments: options?.hasInstructionAttachments ?? false,
};
await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise;
@@ -635,7 +634,7 @@ export class ChatService extends Disposable implements IChatService {
if (agentPart || (defaultAgent && !commandPart)) {
const prepareChatAgentRequest = async (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): Promise<IChatAgentRequest> => {
const initVariableData: IChatRequestVariableData = { variables: [] };
request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, options?.workingSet);
request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation, options?.locationData, options?.attachedContext);
let variableData: IChatRequestVariableData;
let message: string;
@@ -644,11 +643,6 @@ export class ChatService extends Disposable implements IChatService {
message = getPromptText(request.message).message;
} else {
variableData = this.chatVariablesService.resolveVariables(parsedRequest, request.attachedContext);
for (const variable of variableData.variables) {
if (request.workingSet && variable.isFile && URI.isUri(variable.value)) {
request.workingSet.push(variable.value);
}
}
model.updateRequest(request, variableData);
const promptTextResult = getPromptText(request.message);
@@ -74,7 +74,6 @@ export interface IChatRequestViewModel {
readonly variables: IChatRequestVariableEntry[];
currentRenderedHeight: number | undefined;
readonly contentReferences?: ReadonlyArray<IChatContentReference>;
readonly workingSet?: ReadonlyArray<URI>;
readonly confirmation?: string;
readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
readonly isComplete: boolean;
@@ -379,10 +378,6 @@ export class ChatRequestViewModel implements IChatRequestViewModel {
return this._model.response?.contentReferences;
}
get workingSet() {
return this._model.workingSet;
}
get confirmation() {
return this._model.confirmation;
}
@@ -74,8 +74,8 @@ export abstract class ResolveError extends ParseError {
/**
* Error that reflects the case when attempt to open target file fails.
*/
export class FileOpenFailed extends FailedToResolveContentsStream {
public override errorType = 'FileOpenError';
export class OpenFailed extends FailedToResolveContentsStream {
public override errorType = 'OpenError';
constructor(
uri: URI,
@@ -84,7 +84,7 @@ export class FileOpenFailed extends FailedToResolveContentsStream {
super(
uri,
originalError,
`Failed to open file '${uri.toString()}': ${originalError}.`,
`Failed to open '${uri.fsPath}': ${originalError}.`,
);
}
}
@@ -150,11 +150,10 @@ export class RecursiveReference extends ResolveError {
}
/**
* Error that reflects the case when resource URI does not point to
* a prompt snippet file, hence was not attempted to be resolved.
* Error for the case when a resource URI doesn't point to a prompt file.
*/
export class NonPromptSnippetFile extends ResolveError {
public override errorType = 'NonPromptSnippetFileError';
export class NotPromptFile extends ResolveError {
public override errorType = 'NotPromptFileError';
constructor(
uri: URI,
@@ -165,7 +164,27 @@ export class NonPromptSnippetFile extends ResolveError {
super(
uri,
`Resource at ${uri.path} is not a prompt snippet file${suffix}`,
`Resource at ${uri.path} is not a prompt file${suffix}`,
);
}
}
/**
* Error for the case when a resource URI points to a folder.
*/
export class FolderReference extends NotPromptFile {
public override errorType = 'FolderReferenceError';
constructor(
uri: URI,
message: string = '',
) {
const suffix = message ? `: ${message}` : '';
super(
uri,
`Entity at '${uri.path}' is a folder${suffix}`,
);
}
}
@@ -1,310 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from '../../../../../nls.js';
import { DOCUMENTATION_URL, PROMPT_FILE_EXTENSION } from './constants.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
/**
* `!Note!` This doc comment is deprecated and is set to be updated during `debt` week.
* The configuration value can now be one of `{ '/path/to/folder': boolean }` or 'null' types.
* This comment is tracked by [#13119](https://github.com/microsoft/vscode-copilot/issues/13119).
*
* Configuration helper for the `prompt files` feature.
* @see {@link CONFIG_KEY} and {@link DEFAULT_SOURCE_FOLDER}
*
* ### Functions
*
* - {@link getValue} allows to current read configuration value
* - {@link enabled} allows to check if the feature is enabled
* - {@link promptSourceFolders} gets list of source folders for prompt files
*
* ### Configuration Examples
*
* Enable the feature, using defaults for prompt files source folders
* (see {@link DEFAULT_SOURCE_FOLDER}):
* ```json
* {
* "chat.promptFiles": true,
* }
* ```
*
* Enable the feature, specifying a single prompt files source folders,
* in addition to the default `'.github/prompts'` one:
* ```json
* {
* "chat.promptFiles": '.copilot/prompts',
* }
* ```
*
* Enable the feature, specifying multiple prompt files source folders,
* in addition to the default `'.github/prompts'` one:
* ```json
* {
* "chat.promptFiles": {
* ".copilot/prompts" : false,
* "/Users/legomushroom/repos/prompts" : true,
* },
* }
* ```
*
* Enable the feature, specifying multiple prompt files source folders,
* in addition to the default `'.github/prompts'` one:
* ```json
* {
* "chat.promptFiles": [
* ".copilot/prompts",
* "/Users/legomushroom/repos/prompts",
* ],
* }
* ```
*
* The "array" case is similar to the "object" one, but there is one difference.
* At the time of writing, configuration settings with the `array` value cannot
* be merged into a single entry when the setting is specified in both the `user`
* and the `workspace` settings. On the other hand, the "object" case provides
* more flexibility - the settings are combined into a single object.
*
* Enable the feature, using defaults for prompt files source folders
* (see {@link DEFAULT_SOURCE_FOLDER}):
* ```jsonc
* {
* "chat.promptFiles": {}, // same as setting to `true`
* }
* ```
*
* See the next section for details on how we treat the config value.
*
* ### Possible Values
*
* - `undefined`/`null`: feature is disabled
* - `boolean`:
* - `true`: feature is enabled, prompt files source folders
* fallback to {@link DEFAULT_SOURCE_FOLDER}
* - `false`: feature is disabled
* - `string`:
* - values that can be mapped to `boolean`(`"true"`, `"FALSE", "TrUe"`, etc.)
* are treated the same as the `boolean` case above
* - any other `non-empty` string value is treated as a single prompt files source folder path,
* which is used in addition to the default {@link DEFAULT_SOURCE_FOLDER}
* - `empty` string value is treated the same as the `undefined`/`null` case above (disabled)
* - `object`:
* - expects the { "string": `boolean` } pairs, where the `string` is a path and the `boolean`
* is a flag that defines if this additional source folder is enabled or disabled;
* enabled source folders are used in addition to the default {@link DEFAULT_SOURCE_FOLDER} path
* you can explicitly disable the default source folder by setting it to `false` in the object
* - value of a record in the object can also be a `string`:
* - if the string can be clearly mapped to a `boolean` (e.g., `"true"`, `"FALSE", "TrUe"`, etc.),
* it is treated as `boolean` value
* - any other string value is treated as `false` and is effectively ignored
* - if the record `key` is an `empty` string, it is ignored
* - if the resulting object is empty, the feature is considered `enabled`, prompt files source
* folders fallback to {@link DEFAULT_SOURCE_FOLDER}
* - `array`:
* - `string` items(non-empty) in the array are treated as prompt files source folder paths,
* in addition to the default {@link DEFAULT_SOURCE_FOLDER} folder
* - all `non-string` items in the array are `ignored`
* - if the resulting array is empty, the feature is considered `enabled`, prompt files
* source folders fallback to {@link DEFAULT_SOURCE_FOLDER}
*
* ### File Paths Resolution
*
* We resolve only `*.prompt.md` files inside the resulting source folders and
* all `relative` folder paths are resolved relative to:
*
* - the current workspace `root`, if applicable, in other words one of the workspace folders
* can be used as a prompt files source folder
* - root of each top-level folder in the workspace (if there are multiple workspace folders)
* - current root folder (if a single folder is open)
*/
export namespace PromptFilesConfig {
/**
* Configuration key for the `prompt files` feature (also
* known as `prompt files`, `prompt instructions`, etc.).
*/
export const CONFIG_KEY: string = 'chat.promptFiles';
/**
* Default reusable prompt files source folder.
*/
export const DEFAULT_SOURCE_FOLDER = '.github/prompts';
/**
* Get value of the `prompt files` configuration setting.
*/
export const getValue = (
configService: IConfigurationService,
): string | string[] | Record<string, boolean> | boolean | undefined => {
const configValue = configService.getValue(CONFIG_KEY);
if (configValue === undefined || configValue === null) {
return undefined;
}
if (typeof configValue === 'string') {
const trimmedValue = configValue.trim();
const lowercasedValue = trimmedValue.toLowerCase();
if (!lowercasedValue) {
return undefined;
}
if (asBoolean(lowercasedValue) !== undefined) {
return asBoolean(lowercasedValue);
}
return trimmedValue;
}
if (typeof configValue === 'boolean') {
return configValue;
}
if (Array.isArray(configValue)) {
const cleanArray = configValue.filter((item) => {
return typeof item === 'string' && !!item.trim();
});
return cleanArray;
}
// note! this would be also true for `null` and `array`,
// but those cases are already handled above
if (typeof configValue === 'object') {
const paths: Record<string, boolean> = {};
for (const [path, value] of Object.entries(configValue)) {
const cleanPath = path.trim();
const booleanValue = asBoolean(value);
// if value can be mapped to a boolean, and the clean
// path is not empty, add it to the map
if ((booleanValue !== undefined) && cleanPath) {
paths[cleanPath] = booleanValue;
}
}
return paths;
}
return undefined;
};
/**
* Checks if the feature is enabled.
*/
export const enabled = (
configService: IConfigurationService,
): boolean => {
const value = getValue(configService);
return value !== undefined && value !== false;
};
/**
* Gets list of source folders for prompt files.
* Defaults to {@link DEFAULT_SOURCE_FOLDER}.
*/
export const promptSourceFolders = (
configService: IConfigurationService,
): string[] => {
const value = getValue(configService);
if (value === true) {
return [DEFAULT_SOURCE_FOLDER];
}
if (typeof value === 'string') {
const result = [DEFAULT_SOURCE_FOLDER];
const trimmedValue = value.trim();
if (trimmedValue !== DEFAULT_SOURCE_FOLDER) {
result.push(trimmedValue);
}
return result;
}
if (Array.isArray(value)) {
const result = [DEFAULT_SOURCE_FOLDER];
return [
...result,
...value.filter((item) => {
return item !== DEFAULT_SOURCE_FOLDER;
}),
];
}
// note! the `value &&` part handles the `undefined`, `null`, and `false` cases
if (value && (typeof value === 'object')) {
const paths: string[] = [];
// if the default source folder is not explicitly disabled, add it
if (value[DEFAULT_SOURCE_FOLDER] !== false) {
paths.push(DEFAULT_SOURCE_FOLDER);
}
// copy all the enabled paths to the result list
for (const [path, enabled] of Object.entries(value)) {
if (enabled && path !== DEFAULT_SOURCE_FOLDER) {
paths.push(path);
}
}
return paths;
}
// `undefined`, `null`, and `false` cases
return [];
};
/**
* Configuration setting description to use in the settings UI.
*/
export const CONFIG_DESCRIPTION = nls.localize(
'chat.promptFiles.config.description',
"Specify location(s) of reusable prompt files (`*{0}`) that can be attached in Chat, Edits, and Inline Chat sessions. [Learn More]({1}).\n\nRelative paths are resolved from the root folder(s) of your workspace.",
PROMPT_FILE_EXTENSION,
DOCUMENTATION_URL,
);
/**
* Configuration setting title to use in the settings UI.
*/
export const CONFIG_TITLE = nls.localize(
'chat.promptFiles.config.title',
"Prompt Files",
);
}
/**
* Helper to parse an input value of `any` type into a boolean.
*
* @param value - input value to parse
* @returns `true` if the value is the boolean `true` value or a string that can
* be clearly mapped to a boolean (e.g., `"true"`, `"TRUE"`, `"FaLSe"`, etc.),
* `undefined` for rest of the values
*/
function asBoolean(value: any): boolean | undefined {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
const cleanValue = value.trim().toLowerCase();
if (cleanValue === 'true') {
return true;
}
if (cleanValue === 'false') {
return false;
}
return undefined;
}
return undefined;
}
@@ -3,16 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { PROMPT_FILE_EXTENSION } from '../../../../../platform/prompts/common/constants.js';
/**
* Documentation link for the reusable prompts feature.
*/
export const DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets';
/**
* File extension for the reusable prompt files.
*/
export const PROMPT_FILE_EXTENSION = '.prompt.md';
/**
* Prompt files language selector.
*/
@@ -11,7 +11,7 @@ import { CancellationError } from '../../../../../../base/common/errors.js';
import { PromptContentsProviderBase } from './promptContentsProviderBase.js';
import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js';
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
import { FileOpenFailed, NonPromptSnippetFile } from '../../promptFileReferenceErrors.js';
import { OpenFailed, NotPromptFile, ParseError, FolderReference } from '../../promptFileReferenceErrors.js';
import { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js';
/**
@@ -64,14 +64,33 @@ export class FilePromptContentProvider extends PromptContentsProviderBase<FileCh
// get the binary stream of the file contents
let fileStream;
try {
// ensure that the referenced URI points to a file before
// trying to get a stream for its contents
const info = await this.fileService.resolve(this.uri);
// validate that the cancellation was not yet requested
assert(
!cancellationToken?.isCancellationRequested,
new CancellationError(),
);
assert(
info.isFile,
new FolderReference(this.uri),
);
fileStream = await this.fileService.readFileStream(this.uri);
} catch (error) {
throw new FileOpenFailed(this.uri, error);
if (error instanceof ParseError) {
throw error;
}
throw new OpenFailed(this.uri, error);
}
assertDefined(
fileStream,
new FileOpenFailed(this.uri, 'Failed to open file stream.'),
new OpenFailed(this.uri, 'Failed to open file stream.'),
);
// after the promise above complete, this object can be already disposed or
@@ -84,7 +103,7 @@ export class FilePromptContentProvider extends PromptContentsProviderBase<FileCh
// if URI doesn't point to a prompt snippet file, don't try to resolve it
if (!this.isPromptSnippet()) {
throw new NonPromptSnippetFile(this.uri);
throw new NotPromptFile(this.uri);
}
return fileStream.value;
@@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { IPromptContentsProvider } from './types.js';
import { PROMPT_FILE_EXTENSION } from '../constants.js';
import { URI } from '../../../../../../base/common/uri.js';
import { Emitter } from '../../../../../../base/common/event.js';
import { assert } from '../../../../../../base/common/assert.js';
import { CancellationError } from '../../../../../../base/common/errors.js';
import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js';
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js';
import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js';
import { FailedToResolveContentsStream, ParseError } from '../../promptFileReferenceErrors.js';
import { cancelPreviousCalls } from '../../../../../../base/common/decorators/cancelPreviousCalls.js';
@@ -145,6 +145,6 @@ export abstract class PromptContentsProviderBase<
* Check if the current URI points to a prompt snippet.
*/
public isPromptSnippet(): boolean {
return this.uri.path.endsWith(PROMPT_FILE_EXTENSION);
return isPromptFile(this.uri);
}
}
@@ -6,24 +6,24 @@
import { LANGUAGE_SELECTOR } from '../constants.js';
import { IPromptsService } from '../service/types.js';
import { assert } from '../../../../../../base/common/assert.js';
import { FolderReference, NotPromptFile } from '../../promptFileReferenceErrors.js';
import { ITextModel } from '../../../../../../editor/common/model.js';
import { assertDefined } from '../../../../../../base/common/types.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { NonPromptSnippetFile } from '../../promptFileReferenceErrors.js';
import { CancellationError } from '../../../../../../base/common/errors.js';
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
import { Registry } from '../../../../../../platform/registry/common/platform.js';
import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js';
import { ILink, ILinksList, LinkProvider } from '../../../../../../editor/common/languages.js';
import { IWorkbenchContributionsRegistry, Extensions } from '../../../../../common/contributions.js';
import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../common/contributions.js';
/**
* Provides link references for prompt files.
*/
export class PromptLinkProvider extends Disposable implements LinkProvider {
constructor(
@IPromptsService private readonly promptSyntaxService: IPromptsService,
@IPromptsService private readonly promptsService: IPromptsService,
@ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService,
) {
super();
@@ -43,7 +43,7 @@ export class PromptLinkProvider extends Disposable implements LinkProvider {
new CancellationError(),
);
const parser = this.promptSyntaxService.getSyntaxParserFor(model);
const parser = this.promptsService.getSyntaxParserFor(model);
assert(
!parser.disposed,
'Prompt parser must not be disposed.',
@@ -69,10 +69,15 @@ export class PromptLinkProvider extends Disposable implements LinkProvider {
return true;
}
return errorCondition instanceof NonPromptSnippetFile;
// don't provide links for folder references
if (errorCondition instanceof FolderReference) {
return false;
}
return errorCondition instanceof NotPromptFile;
})
.map((reference) => {
const { linkRange } = reference;
const { uri, linkRange } = reference;
// must always be true because of the filter above
assertDefined(
@@ -80,10 +85,9 @@ export class PromptLinkProvider extends Disposable implements LinkProvider {
'Link range must be defined.',
);
return {
range: linkRange,
url: reference.uri,
url: uri,
};
});
@@ -94,5 +98,5 @@ export class PromptLinkProvider extends Disposable implements LinkProvider {
}
// register the provider as a workbench contribution
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench)
.registerWorkbenchContribution(PromptLinkProvider, LifecyclePhase.Eventually);
@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../../../nls.js';
import { PROMPT_FILE_EXTENSION } from '../constants.js';
import { URI } from '../../../../../../base/common/uri.js';
import { ChatPromptCodec } from '../codecs/chatPromptCodec.js';
import { Emitter } from '../../../../../../base/common/event.js';
@@ -19,11 +18,12 @@ import { DeferredPromise } from '../../../../../../base/common/async.js';
import { ILogService } from '../../../../../../platform/log/common/log.js';
import { basename, extUri } from '../../../../../../base/common/resources.js';
import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js';
import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js';
import { ObservableDisposable } from '../../../../../../base/common/observableDisposable.js';
import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js';
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
import { MarkdownLink } from '../../../../../../editor/common/codecs/markdownCodec/tokens/markdownLink.js';
import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js';
import { OpenFailed, NotPromptFile, RecursiveReference, FolderReference, ParseError, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js';
/**
* Well-known localized error messages.
@@ -38,7 +38,7 @@ const errorMessages = {
/**
* Error conditions that may happen during the file reference resolution.
*/
export type TErrorCondition = FileOpenFailed | RecursiveReference | NonPromptSnippetFile;
export type TErrorCondition = OpenFailed | RecursiveReference | FolderReference | NotPromptFile;
/**
* Base prompt parser class that provides a common interface for all
@@ -376,7 +376,18 @@ export abstract class BasePromptParser<T extends IPromptContentsProvider> extend
.filter((reference) => {
const { errorCondition } = reference;
return !errorCondition || (errorCondition instanceof NonPromptSnippetFile);
// include all references without errors
if (!errorCondition) {
return true;
}
// filter out folder references from the list
if (errorCondition instanceof FolderReference) {
return false;
}
// include non-prompt file references
return (errorCondition instanceof NotPromptFile);
});
}
@@ -404,7 +415,7 @@ export abstract class BasePromptParser<T extends IPromptContentsProvider> extend
.filter((childReference) => {
const { errorCondition } = childReference;
return errorCondition && !(errorCondition instanceof NonPromptSnippetFile);
return errorCondition && !(errorCondition instanceof NotPromptFile);
})
// map to error condition objects
.map((childReference): ParseError => {
@@ -471,7 +482,7 @@ export abstract class BasePromptParser<T extends IPromptContentsProvider> extend
* @returns Error message.
*/
protected getErrorMessage<TError extends ParseError>(error: TError): string {
if (error instanceof FileOpenFailed) {
if (error instanceof OpenFailed) {
return `${errorMessages.fileOpenFailed} '${error.uri.path}'.`;
}
@@ -503,18 +514,11 @@ export abstract class BasePromptParser<T extends IPromptContentsProvider> extend
return this.uri.toString() === otherUri.toString();
}
/**
* Check if the provided URI points to a prompt snippet.
*/
public static isPromptSnippet(uri: URI): boolean {
return uri.path.endsWith(PROMPT_FILE_EXTENSION);
}
/**
* Check if the current reference points to a prompt snippet file.
*/
public get isPromptSnippet(): boolean {
return BasePromptParser.isPromptSnippet(this.uri);
return isPromptFile(this.uri);
}
/**
@@ -598,7 +602,7 @@ export class PromptFileReference extends BasePromptParser<FilePromptContentProvi
*/
protected override getErrorMessage(error: ParseError): string {
// if failed to open a file, return approprivate message and the file path
if (error instanceof FileOpenFailed) {
if (error instanceof OpenFailed) {
return `${errorMessages.fileOpenFailed} '${error.uri.path}'.`;
}
@@ -3,12 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { PromptFilesConfig } from '../config.js';
import { PROMPT_FILE_EXTENSION } from '../constants.js';
import { URI } from '../../../../../../base/common/uri.js';
import { ResourceSet } from '../../../../../../base/common/map.js';
import { dirname, extUri } from '../../../../../../base/common/resources.js';
import { IFileService } from '../../../../../../platform/files/common/files.js';
import { PromptsConfig } from '../../../../../../platform/prompts/common/config.js';
import { isPromptFile } from '../../../../../../platform/prompts/common/constants.js';
import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
@@ -71,7 +71,7 @@ export class PromptFilesLocator {
*/
public getConfigBasedSourceFolders(): readonly URI[] {
const paths = new ResourceSet();
const sourceFolders = PromptFilesConfig.promptSourceFolders(this.configService);
const sourceFolders = PromptsConfig.promptSourceFolders(this.configService);
// otherwise for each folder provided in the configuration, create
// a URI per each folder in the current workspace
@@ -154,13 +154,13 @@ export class PromptFilesLocator {
}
for (const child of stat.children) {
const { name, resource, isDirectory } = child;
const { resource, isDirectory } = child;
if (isDirectory) {
continue;
}
if (!name.endsWith(PROMPT_FILE_EXTENSION)) {
if (!isPromptFile(resource)) {
continue;
}
@@ -78,7 +78,6 @@
slashCommands: [ ],
disambiguation: [ ]
},
workingSet: undefined,
slashCommand: undefined,
usedContext: {
documents: [
@@ -78,7 +78,6 @@
slashCommands: [ ],
disambiguation: [ ]
},
workingSet: undefined,
slashCommand: undefined,
usedContext: undefined,
contentReferences: [ ],
@@ -85,7 +85,6 @@
slashCommands: [ ],
disambiguation: [ ]
},
workingSet: undefined,
slashCommand: undefined,
usedContext: {
documents: [
@@ -154,7 +153,6 @@
disambiguation: [ ],
isDefault: true
},
workingSet: undefined,
slashCommand: undefined,
usedContext: undefined,
contentReferences: [ ],
@@ -78,7 +78,6 @@
slashCommands: [ ],
disambiguation: [ ]
},
workingSet: undefined,
slashCommand: undefined,
usedContext: undefined,
contentReferences: [ ],
@@ -1,837 +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 assert from 'assert';
import { mockService } from './testUtils/mock.js';
import { randomInt } from '../../../../../../base/common/numbers.js';
import { PromptFilesConfig } from '../../../common/promptSyntax/config.js';
import { randomBoolean } from '../../../../../../base/test/common/testUtils.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { IConfigurationOverrides, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
/**
* Mocked instance of {@link IConfigurationService}.
*/
const createMock = <T>(value: T): IConfigurationService => {
return mockService<IConfigurationService>({
getValue(key?: string | IConfigurationOverrides) {
assert.strictEqual(
key,
PromptFilesConfig.CONFIG_KEY,
`Mocked service supports only one configuration key: '${PromptFilesConfig.CONFIG_KEY}'.`,
);
return value;
},
});
};
suite('PromptFilesConfig', () => {
ensureNoDisposablesAreLeakedInTestSuite();
suite('• getValue', () => {
test('• undefined', () => {
const configService = createMock(undefined);
assert.strictEqual(
PromptFilesConfig.getValue(configService),
undefined,
'Must read correct value.',
);
});
test('• null', () => {
const configService = createMock(null);
assert.strictEqual(
PromptFilesConfig.getValue(configService),
undefined,
'Must read correct value.',
);
});
suite('• string', () => {
test('• empty', () => {
assert.strictEqual(
PromptFilesConfig.getValue(createMock('')),
undefined,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock(' ')),
undefined,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('\t')),
undefined,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('\v')),
undefined,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('\f')),
undefined,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('\n')),
undefined,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('\r\n')),
undefined,
'Must read correct value.',
);
});
test('• true', () => {
assert.strictEqual(
PromptFilesConfig.getValue(createMock('true')),
true,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('TRUE')),
true,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('TrUe')),
true,
'Must read correct value.',
);
});
test('• false', () => {
assert.strictEqual(
PromptFilesConfig.getValue(createMock('false')),
false,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('FALSE')),
false,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('fAlSe')),
false,
'Must read correct value.',
);
});
test('• non-empty', () => {
assert.strictEqual(
PromptFilesConfig.getValue(createMock('/absolute/path/to/folder')),
'/absolute/path/to/folder',
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('/Absolute/path/to/folder')),
'/Absolute/path/to/folder',
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('./relative-path/to/folder')),
'./relative-path/to/folder',
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('./relative-path/To/folder')),
'./relative-path/To/folder',
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('.github/prompts')),
'.github/prompts',
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('.github/Prompts')),
'.github/Prompts',
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('/abs/path/to/file.prompt.md')),
'/abs/path/to/file.prompt.md',
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock('/Abs/Path/To/File.prompt.md')),
'/Abs/Path/To/File.prompt.md',
'Must read correct value.',
);
});
});
test('• boolean', () => {
assert.strictEqual(
PromptFilesConfig.getValue(createMock(true)),
true,
'Must read correct value.',
);
assert.strictEqual(
PromptFilesConfig.getValue(createMock(false)),
false,
'Must read correct value.',
);
});
suite('• array', () => {
test('• empty', () => {
assert.deepStrictEqual(
PromptFilesConfig.getValue(createMock([])),
[],
'Must read correct value.',
);
});
test('• valid strings', () => {
assert.deepStrictEqual(
PromptFilesConfig.getValue(createMock([
'/absolute/path/to/folder',
'./relative-path/to/folder',
'./another-Relative/Path/to/folder',
'.github/prompts',
'/abs/path/to/file.prompt.md',
'/ABS/path/to/prompts/',
])),
[
'/absolute/path/to/folder',
'./relative-path/to/folder',
'./another-Relative/Path/to/folder',
'.github/prompts',
'/abs/path/to/file.prompt.md',
'/ABS/path/to/prompts/',
],
'Must read correct value.',
);
});
test('• filters out not valid string values', () => {
assert.deepStrictEqual(
PromptFilesConfig.getValue(createMock([
'/usr/local/bin/.hidden-tool',
'../config/.env.example',
[
'test',
randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
],
'',
'./scripts/BUILD.sh',
randomBoolean(),
'/home/user/Documents/report.v1.pdf',
'tmp/.cache/.session.lock',
'/var/log/backup.2025-02-05.log',
{
'key1': randomBoolean(),
'key2': 'value2',
},
'../.git/hooks/pre-commit.sample',
randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
'\t\t',
'/opt/app/data/config.yaml',
'./.config/Subfolder.file',
undefined,
'/usr/share/man/man1/bash.1',
null,
])),
[
'/usr/local/bin/.hidden-tool',
'../config/.env.example',
'./scripts/BUILD.sh',
'/home/user/Documents/report.v1.pdf',
'tmp/.cache/.session.lock',
'/var/log/backup.2025-02-05.log',
'../.git/hooks/pre-commit.sample',
'/opt/app/data/config.yaml',
'./.config/Subfolder.file',
'/usr/share/man/man1/bash.1',
],
'Must read correct value.',
);
});
test('• only invalid values', () => {
assert.deepStrictEqual(
PromptFilesConfig.getValue(createMock([
null,
undefined,
'',
' ',
'\t',
'\v',
'\f',
'\n',
'\r\n',
{
'some-key': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
'another-key': randomBoolean(),
'one_more_key': '../relative/path.to.file',
},
[randomBoolean(), randomBoolean()],
randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
])),
[],
'Must read correct value.',
);
});
});
suite('• object', () => {
test('• empty', () => {
assert.deepStrictEqual(
PromptFilesConfig.getValue(createMock({})),
{},
'Must read correct value.',
);
});
test('• only valid strings', () => {
assert.deepStrictEqual(
PromptFilesConfig.getValue(createMock({
'/root/.bashrc': true,
'../../folder/.hidden-folder/config.xml': true,
'/srv/www/Public_html/.htaccess': true,
'../../another.folder/.WEIRD_FILE.log': true,
'./folder.name/file.name': true,
'/media/external/backup.tar.gz': true,
'/Media/external/.secret.backup': true,
'../relative/path.to.file': true,
'./folderName.with.dots/more.dots.extension': true,
'some/folder.with.dots/another.file': true,
'/var/logs/app.01.05.error': true,
'./.tempfile': true,
})),
{
'/root/.bashrc': true,
'../../folder/.hidden-folder/config.xml': true,
'/srv/www/Public_html/.htaccess': true,
'../../another.folder/.WEIRD_FILE.log': true,
'./folder.name/file.name': true,
'/media/external/backup.tar.gz': true,
'/Media/external/.secret.backup': true,
'../relative/path.to.file': true,
'./folderName.with.dots/more.dots.extension': true,
'some/folder.with.dots/another.file': true,
'/var/logs/app.01.05.error': true,
'./.tempfile': true,
},
'Must read correct value.',
);
});
test('• filters out non valid entries', () => {
assert.deepStrictEqual(
PromptFilesConfig.getValue(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'../assets/img/logo.v2.png': true,
'/mnt/storage/video.archive/episode.01.mkv': false,
'../.local/bin/script.sh': true,
'/usr/local/share/.fonts/CustomFont.otf': '',
'../../development/branch.name/some.test': true,
'/Home/user/.ssh/config': true,
'./hidden.dir/.subhidden': '\f',
'/tmp/.temp.folder/cache.db': true,
'/opt/software/v3.2.1/build.log': ' ',
'': true,
'./scripts/.old.build.sh': true,
'/var/data/datafile.2025-02-05.json': '\n',
'\n\n': true,
'\t': true,
'\v': true,
'\f': true,
'\r\n': true,
'\f\f': true,
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
{
'../assets/img/logo.v2.png': true,
'/mnt/storage/video.archive/episode.01.mkv': false,
'../.local/bin/script.sh': true,
'../../development/branch.name/some.test': true,
'/Home/user/.ssh/config': true,
'/tmp/.temp.folder/cache.db': true,
'./scripts/.old.build.sh': true,
},
'Must read correct value.',
);
});
test('• only invalid or false values', () => {
assert.deepStrictEqual(
PromptFilesConfig.getValue(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'../assets/IMG/logo.v2.png': '',
'/mnt/storage/video.archive/episode.01.mkv': false,
'/usr/local/share/.fonts/CustomFont.otf': '',
'./hidden.dir/.subhidden': '\f',
'/opt/Software/v3.2.1/build.log': ' ',
'/var/data/datafile.2025-02-05.json': '\n',
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
{
'/mnt/storage/video.archive/episode.01.mkv': false,
},
'Must read correct value.',
);
});
});
});
suite('• sourceLocations', () => {
test('• undefined', () => {
const configService = createMock(undefined);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(configService),
[],
'Must read correct value.',
);
});
test('• null', () => {
const configService = createMock(null);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(configService),
[],
'Must read correct value.',
);
});
suite('• string', () => {
test('• empty', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('')),
[],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock(' ')),
[],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('\t')),
[],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('\v')),
[],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('\f')),
[],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('\n')),
[],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('\r\n')),
[],
'Must read correct value.',
);
});
test('• true', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('true')),
['.github/prompts'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('TRUE')),
['.github/prompts'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('TrUe')),
['.github/prompts'],
'Must read correct value.',
);
});
test('• false', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('false')),
[],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('FALSE')),
[],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('fAlSe')),
[],
'Must read correct value.',
);
});
test('• non-empty', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('/absolute/path/to/folder')),
['.github/prompts', '/absolute/path/to/folder'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('/Absolute/path/to/folder')),
['.github/prompts', '/Absolute/path/to/folder'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('./relative-path/to/folder')),
['.github/prompts', './relative-path/to/folder'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('./relative-path/To/folder')),
['.github/prompts', './relative-path/To/folder'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('.github/prompts')),
['.github/prompts'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('.github/Prompts')),
['.github/prompts', '.github/Prompts'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('/abs/path/to/file.prompt.md')),
['.github/prompts', '/abs/path/to/file.prompt.md'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock('/Abs/Path/To/File.prompt.md')),
['.github/prompts', '/Abs/Path/To/File.prompt.md'],
'Must read correct value.',
);
});
});
test('• boolean', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock(true)),
['.github/prompts'],
'Must read correct value.',
);
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock(false)),
[],
'Must read correct value.',
);
});
suite('• array', () => {
test('• empty', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock([])),
['.github/prompts'],
'Must read correct value.',
);
});
test('• valid strings', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock([
'/absolute/path/to/folder',
'./relative-path/to/folder',
'./another-Relative/Path/to/folder',
'.github/prompts',
'/abs/path/to/file.prompt.md',
'.githuB/prompts',
'/ABS/path/to/prompts/',
])),
[
'.github/prompts',
'/absolute/path/to/folder',
'./relative-path/to/folder',
'./another-Relative/Path/to/folder',
'/abs/path/to/file.prompt.md',
'.githuB/prompts',
'/ABS/path/to/prompts/',
],
'Must read correct value.',
);
});
test('• filters out not valid string values', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock([
'/usr/local/bin/.hidden-tool',
'../config/.env.example',
[
'test',
randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
],
'',
'./scripts/BUILD.sh',
randomBoolean(),
'/home/user/Documents/report.v1.pdf',
'tmp/.cache/.session.lock',
'/var/log/backup.2025-02-05.log',
{
'key1': randomBoolean(),
'key2': 'value2',
},
'../.git/hooks/pre-commit.sample',
randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
'\t\t',
'/opt/app/data/config.yaml',
'./.config/Subfolder.file',
undefined,
'/usr/share/man/man1/bash.1',
null,
])),
[
'.github/prompts',
'/usr/local/bin/.hidden-tool',
'../config/.env.example',
'./scripts/BUILD.sh',
'/home/user/Documents/report.v1.pdf',
'tmp/.cache/.session.lock',
'/var/log/backup.2025-02-05.log',
'../.git/hooks/pre-commit.sample',
'/opt/app/data/config.yaml',
'./.config/Subfolder.file',
'/usr/share/man/man1/bash.1',
],
'Must read correct value.',
);
});
test('• only invalid values', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock([
null,
undefined,
'',
' ',
'\t',
'\v',
'\f',
'\n',
'\r\n',
{
'some-key': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
'another-key': randomBoolean(),
'one_more_key': '../relative/path.to.file',
},
[randomBoolean(), randomBoolean()],
randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
])),
['.github/prompts'],
'Must read correct value.',
);
});
});
suite('• object', () => {
test('• empty', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock({})),
['.github/prompts'],
'Must read correct value.',
);
});
test('• only valid strings', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock({
'/root/.bashrc': true,
'../../folder/.hidden-folder/config.xml': true,
'/srv/www/Public_html/.htaccess': true,
'../../another.folder/.WEIRD_FILE.log': true,
'./folder.name/file.name': true,
'/media/external/backup.tar.gz': true,
'/Media/external/.secret.backup': true,
'../relative/path.to.file': true,
'./folderName.with.dots/more.dots.extension': true,
'some/folder.with.dots/another.file': true,
'/var/logs/app.01.05.error': true,
'.GitHub/prompts': true,
'./.tempfile': true,
})),
[
'.github/prompts',
'/root/.bashrc',
'../../folder/.hidden-folder/config.xml',
'/srv/www/Public_html/.htaccess',
'../../another.folder/.WEIRD_FILE.log',
'./folder.name/file.name',
'/media/external/backup.tar.gz',
'/Media/external/.secret.backup',
'../relative/path.to.file',
'./folderName.with.dots/more.dots.extension',
'some/folder.with.dots/another.file',
'/var/logs/app.01.05.error',
'.GitHub/prompts',
'./.tempfile',
],
'Must read correct value.',
);
});
test('• filters out non valid entries', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'../assets/img/logo.v2.png': true,
'/mnt/storage/video.archive/episode.01.mkv': false,
'../.local/bin/script.sh': true,
'/usr/local/share/.fonts/CustomFont.otf': '',
'../../development/branch.name/some.test': true,
'.giThub/prompts': true,
'/Home/user/.ssh/config': true,
'./hidden.dir/.subhidden': '\f',
'/tmp/.temp.folder/cache.db': true,
'.github/prompts': true,
'/opt/software/v3.2.1/build.log': ' ',
'': true,
'./scripts/.old.build.sh': true,
'/var/data/datafile.2025-02-05.json': '\n',
'\n\n': true,
'\t': true,
'\v': true,
'\f': true,
'\r\n': true,
'\f\f': true,
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
[
'.github/prompts',
'../assets/img/logo.v2.png',
'../.local/bin/script.sh',
'../../development/branch.name/some.test',
'.giThub/prompts',
'/Home/user/.ssh/config',
'/tmp/.temp.folder/cache.db',
'./scripts/.old.build.sh',
],
'Must read correct value.',
);
});
test('• only invalid or false values', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'../assets/IMG/logo.v2.png': '',
'/mnt/storage/video.archive/episode.01.mkv': false,
'/usr/local/share/.fonts/CustomFont.otf': '',
'./hidden.dir/.subhidden': '\f',
'/opt/Software/v3.2.1/build.log': ' ',
'/var/data/datafile.2025-02-05.json': '\n',
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
[
'.github/prompts',
],
'Must read correct value.',
);
});
test('• filters out disabled default location', () => {
assert.deepStrictEqual(
PromptFilesConfig.promptSourceFolders(createMock({
'/etc/hosts.backup': '\t\n\t',
'./run.tests.sh': '\v',
'.github/prompts': false,
'../assets/img/logo.v2.png': true,
'/mnt/storage/video.archive/episode.01.mkv': false,
'../.local/bin/script.sh': true,
'/usr/local/share/.fonts/CustomFont.otf': '',
'../../development/branch.name/some.test': true,
'.giThub/prompts': true,
'/Home/user/.ssh/config': true,
'./hidden.dir/.subhidden': '\f',
'/tmp/.temp.folder/cache.db': true,
'/opt/software/v3.2.1/build.log': ' ',
'': true,
'./scripts/.old.build.sh': true,
'/var/data/datafile.2025-02-05.json': '\n',
'\n\n': true,
'\t': true,
'\v': true,
'\f': true,
'\r\n': true,
'\f\f': true,
'../lib/some_library.v1.0.1.so': '\r\n',
'/dev/shm/.shared_resource': randomInt(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER),
})),
[
'../assets/img/logo.v2.png',
'../.local/bin/script.sh',
'../../development/branch.name/some.test',
'.giThub/prompts',
'/Home/user/.ssh/config',
'/tmp/.temp.folder/cache.db',
'./scripts/.old.build.sh',
],
'Must read correct value.',
);
});
});
});
});
@@ -10,7 +10,7 @@ import { Schemas } from '../../../../../../../base/common/network.js';
import { ExpectedReference } from '../testUtils/expectedReference.js';
import { ITextModel } from '../../../../../../../editor/common/model.js';
import { Disposable } from '../../../../../../../base/common/lifecycle.js';
import { FileOpenFailed } from '../../../../common/promptFileReferenceErrors.js';
import { OpenFailed } from '../../../../common/promptFileReferenceErrors.js';
import { IFileService } from '../../../../../../../platform/files/common/files.js';
import { randomBoolean } from '../../../../../../../base/test/common/testUtils.js';
import { FileService } from '../../../../../../../platform/files/common/fileService.js';
@@ -143,7 +143,7 @@ suite('TextModelPromptParser', () => {
startLine: 1,
startColumn: 27,
pathStartColumn: 33,
childrenOrError: new FileOpenFailed(createURI('/abs/path/to/file.md'), 'File not found.'),
childrenOrError: new OpenFailed(createURI('/abs/path/to/file.md'), 'File not found.'),
}),
new ExpectedReference({
uri: createURI('/foo/folder/binary.file'),
@@ -152,7 +152,7 @@ suite('TextModelPromptParser', () => {
startLine: 7,
startColumn: 10,
pathStartColumn: 16,
childrenOrError: new FileOpenFailed(createURI('/foo/folder/binary.file'), 'File not found.'),
childrenOrError: new OpenFailed(createURI('/foo/folder/binary.file'), 'File not found.'),
}),
new ExpectedReference({
uri: createURI('/etc/hosts/random-file.txt'),
@@ -161,7 +161,7 @@ suite('TextModelPromptParser', () => {
startLine: 7,
startColumn: 81,
pathStartColumn: 91,
childrenOrError: new FileOpenFailed(createURI('/etc/hosts/random-file.txt'), 'File not found.'),
childrenOrError: new OpenFailed(createURI('/etc/hosts/random-file.txt'), 'File not found.'),
}),
]);
});
@@ -196,7 +196,7 @@ suite('TextModelPromptParser', () => {
startLine: 3,
startColumn: 43,
pathStartColumn: 55,
childrenOrError: new FileOpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'),
childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/foo-bar-baz/another-file.ts'), 'File not found.'),
}),
new ExpectedReference({
uri: createURI('/absolute/c/file_name.prompt.md'),
@@ -205,7 +205,7 @@ suite('TextModelPromptParser', () => {
startLine: 6,
startColumn: 7,
pathStartColumn: 17,
childrenOrError: new FileOpenFailed(createURI('/absolute/c/file_name.prompt.md'), 'File not found.'),
childrenOrError: new OpenFailed(createURI('/absolute/c/file_name.prompt.md'), 'File not found.'),
}),
new ExpectedReference({
uri: createURI('/absolute/folder/main.rs'),
@@ -214,7 +214,7 @@ suite('TextModelPromptParser', () => {
startLine: 11,
startColumn: 36,
pathStartColumn: 42,
childrenOrError: new FileOpenFailed(createURI('/absolute/folder/main.rs'), 'File not found.'),
childrenOrError: new OpenFailed(createURI('/absolute/folder/main.rs'), 'File not found.'),
}),
new ExpectedReference({
uri: createURI('/absolute/folder/and/a/samefile.jpeg'),
@@ -223,7 +223,7 @@ suite('TextModelPromptParser', () => {
startLine: 11,
startColumn: 56,
pathStartColumn: 62,
childrenOrError: new FileOpenFailed(createURI('/absolute/folder/and/a/samefile.jpeg'), 'File not found.'),
childrenOrError: new OpenFailed(createURI('/absolute/folder/and/a/samefile.jpeg'), 'File not found.'),
}),
]);
});
@@ -25,8 +25,8 @@ import { IConfigurationService } from '../../../../../../platform/configuration/
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
import { ConfigurationService } from '../../../../../../platform/configuration/common/configurationService.js';
import { InMemoryFileSystemProvider } from '../../../../../../platform/files/common/inMemoryFilesystemProvider.js';
import { NonPromptSnippetFile, RecursiveReference, FileOpenFailed } from '../../../common/promptFileReferenceErrors.js';
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { NotPromptFile, RecursiveReference, OpenFailed, FolderReference } from '../../../common/promptFileReferenceErrors.js';
/**
* Represents a file reference with an expected
@@ -147,6 +147,8 @@ class TestPromptFileReference extends Disposable {
/**
* Create expected file reference for testing purposes.
*
* Note! This utility also use for `markdown links` at the moment.
*
* @param filePath The expected path of the file reference (without the `#file:` prefix).
* @param lineNumber The expected line number of the file reference.
* @param startColumnNumber The expected start column number of the file reference.
@@ -187,7 +189,7 @@ suite('PromptFileReference (Unix)', function () {
instantiationService.stub(IConfigurationService, nullConfigService);
});
test('resolves nested file references', async function () {
test('resolves nested file references', async function () {
if (isWindows) {
this.skip();
}
@@ -216,14 +218,14 @@ suite('PromptFileReference (Unix)', function () {
children: [
{
name: 'file3.prompt.md',
contents: `\n\n\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents\n some more\t content`,
contents: `\n[](./some-other-folder/non-existing-folder)\n\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.prompt.md contents\n some more\t content`,
},
{
name: 'some-other-folder',
children: [
{
name: 'file4.prompt.md',
contents: 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference\n\n\nand some\n non-prompt #file:./some-non-prompt-file.md',
contents: 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference\n\n\nand some\n non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t',
},
{
name: 'file.txt',
@@ -234,7 +236,7 @@ suite('PromptFileReference (Unix)', function () {
children: [
{
name: 'another-file.prompt.md',
contents: 'another-file.prompt.md contents\t [#file:file.txt](../file.txt)',
contents: `[](${rootFolder}/folder1/some-other-folder)\nanother-file.prompt.md contents\t [#file:file.txt](../file.txt)`,
},
{
name: 'one_more_file_just_in_case.prompt.md',
@@ -260,6 +262,18 @@ suite('PromptFileReference (Unix)', function () {
rootUri,
createTestFileReference('folder1/file3.prompt.md', 2, 14),
),
new ExpectedReference(
URI.joinPath(rootUri, './folder1'),
createTestFileReference(
`./some-other-folder/non-existing-folder`,
2,
1,
),
new OpenFailed(
URI.joinPath(rootUri, './folder1/some-other-folder/non-existing-folder'),
'Reference to non-existing file cannot be opened.',
),
),
new ExpectedReference(
URI.joinPath(rootUri, './folder1'),
createTestFileReference(
@@ -268,10 +282,18 @@ suite('PromptFileReference (Unix)', function () {
26,
),
),
new ExpectedReference(
URI.joinPath(rootUri, './folder1/some-other-folder'),
createTestFileReference('.', 1, 1),
new FolderReference(
URI.joinPath(rootUri, './folder1/some-other-folder'),
'This folder is not a prompt file!',
),
),
new ExpectedReference(
URI.joinPath(rootUri, './folder1/some-other-folder/yetAnotherFolder🤭'),
createTestFileReference('../file.txt', 1, 35),
new NonPromptSnippetFile(
createTestFileReference('../file.txt', 2, 35),
new NotPromptFile(
URI.joinPath(rootUri, './folder1/some-other-folder/file.txt'),
'Ughh oh, that is not a prompt file!',
),
@@ -283,7 +305,7 @@ suite('PromptFileReference (Unix)', function () {
new ExpectedReference(
URI.joinPath(rootUri, './folder1/some-other-folder'),
createTestFileReference('./some-non-existing/file.prompt.md', 1, 30),
new FileOpenFailed(
new OpenFailed(
URI.joinPath(rootUri, './folder1/some-other-folder/some-non-existing/file.prompt.md'),
'Failed to open non-existring prompt snippets file',
),
@@ -291,18 +313,26 @@ suite('PromptFileReference (Unix)', function () {
new ExpectedReference(
URI.joinPath(rootUri, './folder1/some-other-folder'),
createTestFileReference('./some-non-prompt-file.md', 5, 13),
new FileOpenFailed(
new OpenFailed(
URI.joinPath(rootUri, './folder1/some-other-folder/some-non-prompt-file.md'),
'Oh no!',
),
),
new ExpectedReference(
URI.joinPath(rootUri, './some-other-folder/folder1'),
createTestFileReference('../../folder1', 5, 48),
new FolderReference(
URI.joinPath(rootUri, './folder1'),
'Uggh ohh!',
),
),
]
));
await test.run();
});
test('does not fall into infinite reference recursion', async function () {
test('does not fall into infinite reference recursion', async function () {
if (isWindows) {
this.skip();
}
@@ -409,7 +439,7 @@ suite('PromptFileReference (Unix)', function () {
new ExpectedReference(
URI.joinPath(rootUri, './folder1/some-other-folder'),
createTestFileReference('../some-non-existing/file.prompt.md', 1, 30),
new FileOpenFailed(
new OpenFailed(
URI.joinPath(rootUri, './folder1/some-non-existing/file.prompt.md'),
'Uggh ohh!',
),
@@ -442,7 +472,7 @@ suite('PromptFileReference (Unix)', function () {
new ExpectedReference(
rootUri,
createTestFileReference('./file1.md', 6, 2),
new NonPromptSnippetFile(
new NotPromptFile(
URI.joinPath(rootUri, './file1.md'),
'Uggh oh!',
),
@@ -52,11 +52,11 @@ export class CommentFormActions implements IDisposable {
const button = dropDownActions.length ? new ButtonWithDropdown(this.container, {
contextMenuProvider: this.contextMenuService,
actions: dropDownActions,
actionRunner: new class extends ActionRunner {
actionRunner: this._toDispose.add(new class extends ActionRunner {
protected override async runAction(action: IAction, context?: unknown): Promise<void> {
return actionHandler(action);
}
},
}),
secondary: !isPrimary,
title,
addPrimaryActionToDropdown: false,

Some files were not shown because too many files have changed in this diff Show More