mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-08 15:55:15 +01:00
Merge remote-tracking branch 'origin/main' into tyriar/241128__better_parse
This commit is contained in:
+1
-1
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Generated
-10
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
+4
-4
@@ -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(),
|
||||
|
||||
+6
-1
@@ -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);
|
||||
});
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+4
-4
@@ -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();
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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);
|
||||
}
|
||||
+2
-2
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -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() {
|
||||
|
||||
+9
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+168
-15
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
+3
-4
@@ -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),
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -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.
|
||||
*/
|
||||
|
||||
+23
-4
@@ -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;
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -78,7 +78,6 @@
|
||||
slashCommands: [ ],
|
||||
disambiguation: [ ]
|
||||
},
|
||||
workingSet: undefined,
|
||||
slashCommand: undefined,
|
||||
usedContext: {
|
||||
documents: [
|
||||
|
||||
-1
@@ -78,7 +78,6 @@
|
||||
slashCommands: [ ],
|
||||
disambiguation: [ ]
|
||||
},
|
||||
workingSet: undefined,
|
||||
slashCommand: undefined,
|
||||
usedContext: undefined,
|
||||
contentReferences: [ ],
|
||||
|
||||
-2
@@ -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: [ ],
|
||||
|
||||
-1
@@ -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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+8
-8
@@ -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.'),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
+42
-12
@@ -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!',
|
||||
),
|
||||
|
||||
+4
-1246
File diff suppressed because it is too large
Load Diff
@@ -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
Reference in New Issue
Block a user